mediasfu-shared 1.2.2 → 1.2.4

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 (94) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/dist/audioProcessing-0z5KlcmO.js +35 -0
  3. package/dist/audioProcessing-0z5KlcmO.js.map +1 -0
  4. package/dist/audioProcessing-BpbrdCk-.cjs +34 -0
  5. package/dist/audioProcessing-BpbrdCk-.cjs.map +1 -0
  6. package/dist/consumers/audioProcessing.d.ts +8 -0
  7. package/dist/consumers/audioProcessing.d.ts.map +1 -0
  8. package/dist/consumers/connectIps.d.ts.map +1 -1
  9. package/dist/consumers/createSendTransport.d.ts.map +1 -1
  10. package/dist/consumers/gridLayout/addVideosGrid.engine.d.ts +6 -0
  11. package/dist/consumers/gridLayout/addVideosGrid.engine.d.ts.map +1 -1
  12. package/dist/consumers/index.cjs +3 -2
  13. package/dist/consumers/index.cjs.map +1 -1
  14. package/dist/consumers/index.js +14 -13
  15. package/dist/consumers/signalNewConsumerTransport.d.ts.map +1 -1
  16. package/dist/consumers/switchUserAudio.d.ts +3 -1
  17. package/dist/consumers/switchUserAudio.d.ts.map +1 -1
  18. package/dist/consumers/transportRecovery.d.ts +8 -0
  19. package/dist/consumers/transportRecovery.d.ts.map +1 -0
  20. package/dist/{getParticipantMedia-BH3d_-Q1.cjs → getParticipantMedia-CP4hE5Rg.cjs} +9 -4
  21. package/dist/getParticipantMedia-CP4hE5Rg.cjs.map +1 -0
  22. package/dist/{getParticipantMedia-CGqnSb93.js → getParticipantMedia-CvXdDj03.js} +9 -4
  23. package/dist/getParticipantMedia-CvXdDj03.js.map +1 -0
  24. package/dist/index.cjs +138 -25
  25. package/dist/index.cjs.map +1 -1
  26. package/dist/index.d.ts +2 -0
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +205 -92
  29. package/dist/index.js.map +1 -1
  30. package/dist/index.native.cjs +8 -7
  31. package/dist/index.native.cjs.map +1 -1
  32. package/dist/index.native.js +19 -18
  33. package/dist/{joinLocalRoom-7ibqv7zZ.js → joinLocalRoom-Cx1y3Ojj.js} +4 -4
  34. package/dist/{joinLocalRoom-7ibqv7zZ.js.map → joinLocalRoom-Cx1y3Ojj.js.map} +1 -1
  35. package/dist/{joinLocalRoom-BJBDvTYh.cjs → joinLocalRoom-DNJ2zb8W.cjs} +10 -10
  36. package/dist/{joinLocalRoom-BJBDvTYh.cjs.map → joinLocalRoom-DNJ2zb8W.cjs.map} +1 -1
  37. package/dist/{joinRoomOnMediaSFU-y6vJ1OBo.js → joinRoomOnMediaSFU-BnAWLciF.js} +9 -3
  38. package/dist/joinRoomOnMediaSFU-BnAWLciF.js.map +1 -0
  39. package/dist/{joinRoomOnMediaSFU-CC8RVJgr.cjs → joinRoomOnMediaSFU-D2C7q1GK.cjs} +9 -3
  40. package/dist/joinRoomOnMediaSFU-D2C7q1GK.cjs.map +1 -0
  41. package/dist/methods/index.cjs +3 -3
  42. package/dist/methods/index.js +3 -3
  43. package/dist/methods/stream/clickAudio.d.ts +3 -1
  44. package/dist/methods/stream/clickAudio.d.ts.map +1 -1
  45. package/dist/methods/stream/switchAudio.d.ts +3 -1
  46. package/dist/methods/stream/switchAudio.d.ts.map +1 -1
  47. package/dist/methods/utils/headless/virtualBackground.d.ts +15 -1
  48. package/dist/methods/utils/headless/virtualBackground.d.ts.map +1 -1
  49. package/dist/methods/utils/virtualBackgroundCompositor.d.ts +4 -0
  50. package/dist/methods/utils/virtualBackgroundCompositor.d.ts.map +1 -1
  51. package/dist/methods/utils/virtualBackgroundFrameLoop.d.ts +10 -0
  52. package/dist/methods/utils/virtualBackgroundFrameLoop.d.ts.map +1 -0
  53. package/dist/sockets/SocketManager.d.ts +0 -27
  54. package/dist/sockets/SocketManager.d.ts.map +1 -1
  55. package/dist/{translationConsumerSwitch-C20MfNNq.cjs → translationConsumerSwitch-BAsQUpH6.cjs} +9 -1
  56. package/dist/translationConsumerSwitch-BAsQUpH6.cjs.map +1 -0
  57. package/dist/{translationConsumerSwitch-zKuS5wgH.js → translationConsumerSwitch-BUODiT6o.js} +20 -12
  58. package/dist/translationConsumerSwitch-BUODiT6o.js.map +1 -0
  59. package/dist/types/index.cjs +3 -3
  60. package/dist/types/index.js +3 -3
  61. package/dist/types/shared-base-types.d.ts +12 -0
  62. package/dist/types/shared-base-types.d.ts.map +1 -1
  63. package/dist/{updateParticipantAudioDecibels-bZ6R8WHj.cjs → updateParticipantAudioDecibels-BJ2PxJJD.cjs} +287 -58
  64. package/dist/updateParticipantAudioDecibels-BJ2PxJJD.cjs.map +1 -0
  65. package/dist/{updateParticipantAudioDecibels-C_MygF6q.js → updateParticipantAudioDecibels-DiJ_LFfH.js} +284 -55
  66. package/dist/updateParticipantAudioDecibels-DiJ_LFfH.js.map +1 -0
  67. package/package.json +1 -1
  68. package/src/consumers/audioProcessing.ts +26 -0
  69. package/src/consumers/connectIps.ts +46 -7
  70. package/src/consumers/createSendTransport.ts +5 -20
  71. package/src/consumers/gridLayout/addVideosGrid.engine.ts +13 -0
  72. package/src/consumers/signalNewConsumerTransport.ts +7 -25
  73. package/src/consumers/switchUserAudio.ts +18 -5
  74. package/src/consumers/transportRecovery.ts +170 -0
  75. package/src/index.ts +2 -0
  76. package/src/methods/stream/clickAudio.ts +13 -5
  77. package/src/methods/stream/switchAudio.ts +10 -1
  78. package/src/methods/utils/headless/virtualBackground.ts +55 -15
  79. package/src/methods/utils/virtualBackgroundCompositor.ts +18 -1
  80. package/src/methods/utils/virtualBackgroundFrameLoop.ts +69 -0
  81. package/src/sockets/SocketManager.ts +54 -7
  82. package/src/types/shared-base-types.ts +3 -0
  83. package/dist/getParticipantMedia-BH3d_-Q1.cjs.map +0 -1
  84. package/dist/getParticipantMedia-CGqnSb93.js.map +0 -1
  85. package/dist/joinRoomOnMediaSFU-CC8RVJgr.cjs.map +0 -1
  86. package/dist/joinRoomOnMediaSFU-y6vJ1OBo.js.map +0 -1
  87. package/dist/translationConsumerSwitch-C20MfNNq.cjs.map +0 -1
  88. package/dist/translationConsumerSwitch-zKuS5wgH.js.map +0 -1
  89. package/dist/updateParticipantAudioDecibels-C_MygF6q.js.map +0 -1
  90. package/dist/updateParticipantAudioDecibels-bZ6R8WHj.cjs.map +0 -1
  91. package/dist/validateAlphanumeric-DKn5BsUP.cjs +0 -15
  92. package/dist/validateAlphanumeric-DKn5BsUP.cjs.map +0 -1
  93. package/dist/validateAlphanumeric-Dk1JB80W.js +0 -16
  94. package/dist/validateAlphanumeric-Dk1JB80W.js.map +0 -1
@@ -3,7 +3,11 @@ 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';
10
+ import { startVirtualBackgroundFrameLoop } from '../virtualBackgroundFrameLoop';
7
11
 
8
12
  const MEDIAPIPE_CDN = 'https://cdn.jsdelivr.net/npm/@mediapipe/selfie_segmentation';
9
13
 
@@ -57,6 +61,13 @@ export interface ApplyVirtualBackgroundOptions extends HeadlessOptions {
57
61
  * A trailing slash is added if missing.
58
62
  */
59
63
  assetPath?: string;
64
+ /**
65
+ * Blur the area behind the segmented person by this many pixels. When this
66
+ * is greater than zero it takes precedence over `image`. Defaults to 0.
67
+ */
68
+ blurPixels?: number;
69
+ /** Best-effort hidden-tab processing for blur and image backgrounds. Defaults to true. */
70
+ keepProcessingWhenHidden?: boolean;
60
71
  }
61
72
 
62
73
  export type ApplyVirtualBackgroundType = (
@@ -98,6 +109,8 @@ export async function applyVirtualBackground({
98
109
  modelSelection = 1,
99
110
  publish = true,
100
111
  assetPath = MEDIAPIPE_CDN,
112
+ blurPixels = 0,
113
+ keepProcessingWhenHidden = true,
101
114
  }: ApplyVirtualBackgroundOptions): Promise<HeadlessActionResult & { stream: MediaStream | null }> {
102
115
  const live = getCurrentParams({ parameters });
103
116
  if (live.audioOnlyRoom) {
@@ -124,7 +137,10 @@ export async function applyVirtualBackground({
124
137
  const width = Number(settings.width) || 640;
125
138
  const height = Number(settings.height) || 360;
126
139
 
127
- const backgroundImage = typeof image === 'string' ? await loadImage(image) : image;
140
+ const resolvedBlurPixels = Math.max(0, Number(blurPixels) || 0);
141
+ const backgroundImage = resolvedBlurPixels > 0
142
+ ? null
143
+ : (typeof image === 'string' ? await loadImage(image) : image);
128
144
  const producer = publish ? (live.videoProducer || live.localVideoProducer) : null;
129
145
 
130
146
  // mediasoup Producers normally own their track (`stopTracks: true`). Their
@@ -169,6 +185,7 @@ export async function applyVirtualBackground({
169
185
  backgroundImage: backgroundImage as CanvasImageSource | null,
170
186
  width: canvas.width,
171
187
  height: canvas.height,
188
+ blurFallbackPixels: resolvedBlurPixels,
172
189
  });
173
190
  } catch {
174
191
  // A dropped frame must never kill the loop.
@@ -176,27 +193,31 @@ export async function applyVirtualBackground({
176
193
  });
177
194
 
178
195
  let stopped = false;
179
- let frameHandle = 0;
180
- const pump = async () => {
181
- if (stopped) return;
182
- try {
183
- if (video.readyState >= 2) await segmentation.send({ image: video });
184
- } catch {
185
- // Ignore a single failed frame; the next one is already queued.
186
- }
187
- frameHandle = window.requestAnimationFrame(() => { void pump(); });
188
- };
189
- void pump();
196
+ const stopFrameLoop = startVirtualBackgroundFrameLoop({
197
+ owner: segmentation,
198
+ keepProcessingWhenHidden,
199
+ shouldContinue: () => !stopped && processingTrack.readyState === 'live',
200
+ processFrame: () => video.readyState >= 2
201
+ ? segmentation.send({ image: video })
202
+ : undefined,
203
+ });
190
204
 
191
- const processed: MediaStream = (canvas as any).captureStream(frameRate);
205
+ let processed: MediaStream;
206
+ try {
207
+ processed = (canvas as any).captureStream(frameRate);
208
+ } catch (error) {
209
+ stopFrameLoop();
210
+ throw error;
211
+ }
192
212
  if (!processed?.getVideoTracks?.().length) {
213
+ stopFrameLoop();
193
214
  throw new Error('The processed background stream produced no video track.');
194
215
  }
195
216
 
196
217
  const stop = () => {
197
218
  if (stopped) return;
198
219
  stopped = true;
199
- window.cancelAnimationFrame(frameHandle);
220
+ stopFrameLoop();
200
221
  try { segmentation.close(); } catch { /* already closed */ }
201
222
  processed.getTracks().forEach((track) => track.stop());
202
223
  processingTrack.stop();
@@ -236,6 +257,25 @@ export async function applyVirtualBackground({
236
257
  }
237
258
  }
238
259
 
260
+ export interface ApplyBackgroundBlurOptions extends Omit<ApplyVirtualBackgroundOptions, 'image'> {
261
+ /** Blur strength in CSS pixels. Defaults to 16. */
262
+ blurPixels?: number;
263
+ }
264
+
265
+ /** Apply person-aware background blur through the standard processed-track lifecycle. */
266
+ export function applyBackgroundBlur({
267
+ blurPixels = DEFAULT_BACKGROUND_BLUR_PIXELS,
268
+ ...options
269
+ }: ApplyBackgroundBlurOptions): ReturnType<ApplyVirtualBackgroundType> {
270
+ return applyVirtualBackground({
271
+ ...options,
272
+ image: null,
273
+ blurPixels,
274
+ });
275
+ }
276
+
277
+ export type ApplyBackgroundBlurType = typeof applyBackgroundBlur;
278
+
239
279
  export type ClearVirtualBackgroundType = (
240
280
  options: HeadlessOptions
241
281
  ) => 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';
@@ -0,0 +1,69 @@
1
+ export interface VirtualBackgroundFrameLoopOptions {
2
+ owner: object;
3
+ processFrame: () => Promise<unknown> | unknown;
4
+ shouldContinue: () => boolean;
5
+ /** Best effort: background tabs may still throttle timers and camera capture. */
6
+ keepProcessingWhenHidden?: boolean;
7
+ }
8
+
9
+ const activeLoops = new WeakMap<object, () => void>();
10
+ const activeFrames = new WeakMap<object, Promise<unknown>>();
11
+
12
+ /** Run at most one segmentation frame at a time, including in hidden tabs. */
13
+ export function startVirtualBackgroundFrameLoop({
14
+ owner,
15
+ processFrame,
16
+ shouldContinue,
17
+ keepProcessingWhenHidden = true,
18
+ }: VirtualBackgroundFrameLoopOptions): () => void {
19
+ activeLoops.get(owner)?.();
20
+ let stopped = false;
21
+ let inFlight = false;
22
+ let animationFrameId: number | null = null;
23
+ let timeoutId: ReturnType<typeof setTimeout> | null = null;
24
+
25
+ const cancelScheduledFrame = () => {
26
+ if (animationFrameId !== null) cancelAnimationFrame(animationFrameId);
27
+ if (timeoutId !== null) clearTimeout(timeoutId);
28
+ animationFrameId = null;
29
+ timeoutId = null;
30
+ };
31
+ const stop = () => {
32
+ if (stopped) return;
33
+ stopped = true;
34
+ cancelScheduledFrame();
35
+ document.removeEventListener('visibilitychange', onVisibilityChange);
36
+ if (activeLoops.get(owner) === stop) activeLoops.delete(owner);
37
+ };
38
+ const schedule = () => {
39
+ if (stopped || inFlight) return;
40
+ if (!shouldContinue()) { stop(); return; }
41
+ if (document.hidden && keepProcessingWhenHidden) {
42
+ timeoutId = setTimeout(() => { void runFrame(); }, 1000);
43
+ } else {
44
+ animationFrameId = requestAnimationFrame(() => { void runFrame(); });
45
+ }
46
+ };
47
+ const runFrame = async () => {
48
+ animationFrameId = null;
49
+ timeoutId = null;
50
+ if (stopped || inFlight) return;
51
+ if (!shouldContinue()) { stop(); return; }
52
+ inFlight = true;
53
+ try {
54
+ const previousFrame = activeFrames.get(owner);
55
+ if (previousFrame) await previousFrame.catch(() => undefined);
56
+ if (stopped || !shouldContinue()) { stop(); return; }
57
+ const currentFrame = Promise.resolve(processFrame());
58
+ activeFrames.set(owner, currentFrame);
59
+ try { await currentFrame; }
60
+ finally { if (activeFrames.get(owner) === currentFrame) activeFrames.delete(owner); }
61
+ } catch { /* A dropped frame is recoverable. */ }
62
+ finally { inFlight = false; schedule(); }
63
+ };
64
+ const onVisibilityChange = () => { cancelScheduledFrame(); schedule(); };
65
+ activeLoops.set(owner, stop);
66
+ document.addEventListener('visibilitychange', onVisibilityChange);
67
+ void runFrame();
68
+ return stop;
69
+ }
@@ -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
 
@@ -240,6 +240,7 @@ export type SeedData = {
240
240
  };
241
241
 
242
242
  export interface MeetingRoomParams {
243
+ backendAudioDenoise?: { enabled: boolean; profile?: 'arnndn' }; // Optional; omitted uses the account preset.
243
244
  itemPageLimit: number;
244
245
  mediaType: 'audio' | 'video';
245
246
  addCoHost: boolean;
@@ -316,6 +317,7 @@ export type ScreenParamsType = {
316
317
  };
317
318
 
318
319
  export interface CreateRoomOptions {
320
+ backendAudioDenoise?: { enabled: boolean; profile?: 'arnndn' };
319
321
  action: 'create' | 'join';
320
322
  meetingID: string;
321
323
  duration: number;
@@ -339,6 +341,7 @@ export interface CreateRoomOptions {
339
341
  }
340
342
 
341
343
  export interface CreateMediaSFURoomOptions {
344
+ backendAudioDenoise?: { enabled: boolean; profile?: 'arnndn' };
342
345
  action: 'create';
343
346
  duration: number;
344
347
  capacity: number;