mediasfu-shared 1.3.0 → 1.3.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.
Files changed (41) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/consumers/connectSendTransportAudio.d.ts.map +1 -1
  3. package/dist/consumers/connectSendTransportScreen.d.ts.map +1 -1
  4. package/dist/consumers/connectSendTransportVideo.d.ts.map +1 -1
  5. package/dist/consumers/createSendTransport.d.ts +4 -48
  6. package/dist/consumers/createSendTransport.d.ts.map +1 -1
  7. package/dist/consumers/index.cjs +1 -1
  8. package/dist/consumers/index.js +1 -1
  9. package/dist/consumers/sendTransportGuard.d.ts +30 -0
  10. package/dist/consumers/sendTransportGuard.d.ts.map +1 -0
  11. package/dist/{getParticipantMedia-DEJwXXhU.js → getParticipantMedia-CZbsFBth.js} +3 -3
  12. package/dist/{getParticipantMedia-DEJwXXhU.js.map → getParticipantMedia-CZbsFBth.js.map} +1 -1
  13. package/dist/{getParticipantMedia-1okyF4H7.cjs → getParticipantMedia-Dli4dOLp.cjs} +3 -3
  14. package/dist/{getParticipantMedia-1okyF4H7.cjs.map → getParticipantMedia-Dli4dOLp.cjs.map} +1 -1
  15. package/dist/index.cjs +4 -3
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.js +9 -8
  18. package/dist/index.native.cjs +4 -3
  19. package/dist/index.native.cjs.map +1 -1
  20. package/dist/index.native.js +8 -7
  21. package/dist/{joinLocalRoom-DS6_6lJL.js → joinLocalRoom-BWoS-8wt.js} +2 -2
  22. package/dist/{joinLocalRoom-DS6_6lJL.js.map → joinLocalRoom-BWoS-8wt.js.map} +1 -1
  23. package/dist/{joinLocalRoom-Dsn39SwS.cjs → joinLocalRoom-DQvud99t.cjs} +2 -2
  24. package/dist/{joinLocalRoom-Dsn39SwS.cjs.map → joinLocalRoom-DQvud99t.cjs.map} +1 -1
  25. package/dist/sockets/SocketManager.d.ts +7 -0
  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-Dni8i6AY.cjs → updateParticipantAudioDecibels-CbCP1PDs.cjs} +162 -67
  30. package/dist/updateParticipantAudioDecibels-CbCP1PDs.cjs.map +1 -0
  31. package/dist/{updateParticipantAudioDecibels-BBfdHWQ0.js → updateParticipantAudioDecibels-xKKq7Zp5.js} +166 -71
  32. package/dist/updateParticipantAudioDecibels-xKKq7Zp5.js.map +1 -0
  33. package/package.json +1 -1
  34. package/src/consumers/connectSendTransportAudio.ts +7 -4
  35. package/src/consumers/connectSendTransportScreen.ts +6 -4
  36. package/src/consumers/connectSendTransportVideo.ts +4 -2
  37. package/src/consumers/createSendTransport.ts +62 -4
  38. package/src/consumers/sendTransportGuard.ts +65 -0
  39. package/src/sockets/SocketManager.ts +31 -1
  40. package/dist/updateParticipantAudioDecibels-BBfdHWQ0.js.map +0 -1
  41. package/dist/updateParticipantAudioDecibels-Dni8i6AY.cjs.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mediasfu-shared",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
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",
@@ -1,4 +1,5 @@
1
1
  import type { Transport, Producer, ProducerOptions } from 'mediasoup-client/types';
2
+ import { produceOnce } from "./sendTransportGuard";
2
3
 
3
4
  export interface ConnectSendTransportAudioParameters {
4
5
  audioProducer: Producer | null;
@@ -39,7 +40,7 @@ const connectLocalSendTransportAudio = async ({
39
40
  } = parameters;
40
41
 
41
42
  if (localProducerTransport) {
42
- localAudioProducer = await localProducerTransport.produce(audioParams);
43
+ localAudioProducer = (await produceOnce(localProducerTransport, "audio", () => localProducerTransport!.produce(audioParams))).producer;
43
44
 
44
45
  updateLocalAudioProducer?.(localAudioProducer);
45
46
  updateLocalProducerTransport?.(localProducerTransport);
@@ -127,10 +128,12 @@ export const connectSendTransportAudio: ConnectSendTransportAudioType = async ({
127
128
 
128
129
  // Attempt to connect the primary send transport
129
130
  if (targetOption === "all" || targetOption === "remote") {
130
- audioProducer = await producerTransport!.produce(audioParams);
131
+ // A concurrent call for the same transport shares the producer instead of creating a second one.
132
+ const produced = await produceOnce(producerTransport!, "audio", () => producerTransport!.produce(audioParams));
133
+ audioProducer = produced.producer;
131
134
 
132
- // Update the audio level
133
- updateMicLevel(audioProducer, parameters.updateAudioLevel);
135
+ // Update the audio level (once per producer)
136
+ if (!produced.joined) updateMicLevel(audioProducer, parameters.updateAudioLevel);
134
137
 
135
138
  // Update state with the new producer and transport
136
139
  updateAudioProducer(audioProducer);
@@ -1,4 +1,5 @@
1
1
  import type { Transport, Producer, Device, ProducerOptions } from 'mediasoup-client/types';
2
+ import { produceOnce } from "./sendTransportGuard";
2
3
 
3
4
  export interface ConnectSendTransportScreenParameters {
4
5
  screenProducer: Producer | null;
@@ -48,11 +49,11 @@ const connectLocalSendTransportScreen = async ({
48
49
 
49
50
  // Produce local screen share data
50
51
  if (localProducerTransport) {
51
- localScreenProducer = await localProducerTransport.produce({
52
+ localScreenProducer = (await produceOnce(localProducerTransport, "screen", () => localProducerTransport!.produce({
52
53
  track: stream.getVideoTracks()[0],
53
54
  codec,
54
55
  appData: { mediaTag: "screen-video" },
55
- });
56
+ }))).producer;
56
57
 
57
58
  // Update the local producer and transport objects
58
59
  updateLocalScreenProducer?.(localScreenProducer);
@@ -144,12 +145,13 @@ export const connectSendTransportScreen: ConnectSendTransportScreenType = async
144
145
 
145
146
  // Produce screen share data using the producer transport
146
147
  if (targetOption === "remote" || targetOption === "all") {
147
- screenProducer = await producerTransport!.produce({
148
+ // A concurrent call for the same transport shares the producer instead of creating a second one.
149
+ screenProducer = (await produceOnce(producerTransport!, "screen", () => producerTransport!.produce({
148
150
  track: stream.getVideoTracks()[0],
149
151
  ...params,
150
152
  codec,
151
153
  appData: { mediaTag: "screen-video" },
152
- });
154
+ }))).producer;
153
155
 
154
156
  // Update the screen producer and producer transport objects
155
157
  updateScreenProducer(screenProducer);
@@ -1,4 +1,5 @@
1
1
  import type { Device, Producer, ProducerOptions, Transport } from 'mediasoup-client/types';
2
+ import { produceOnce } from "./sendTransportGuard";
2
3
 
3
4
  export interface ConnectSendTransportVideoParameters {
4
5
  videoProducer: Producer | null;
@@ -39,7 +40,7 @@ const connectLocalSendTransportVideo = async ({
39
40
 
40
41
  // Produce local video data if transport exists
41
42
  if (localProducerTransport) {
42
- localVideoProducer = await localProducerTransport.produce(videoParams);
43
+ localVideoProducer = (await produceOnce(localProducerTransport, "video", () => localProducerTransport!.produce(videoParams))).producer;
43
44
 
44
45
  // Update local producer and transport
45
46
  updateLocalVideoProducer?.(localVideoProducer);
@@ -121,7 +122,8 @@ export const connectSendTransportVideo: ConnectSendTransportVideoType = async ({
121
122
 
122
123
  // Produce video data using the primary transport
123
124
  if (targetOption === "all" || targetOption === "remote"){
124
- videoProducer = await producerTransport!.produce(videoParams);
125
+ // A concurrent call for the same transport shares the producer instead of creating a second one.
126
+ videoProducer = (await produceOnce(producerTransport!, "video", () => producerTransport!.produce(videoParams))).producer;
125
127
 
126
128
  // Update main window state based on the video connection level
127
129
  if (islevel === "2") {
@@ -1,5 +1,6 @@
1
1
  import type { Device, Transport, DtlsParameters } from 'mediasoup-client/types';
2
2
  import { attachTransportRecovery, clearFailedSendState } from './transportRecovery';
3
+ import { isOpen, pendingSendTransportCreation, trackSendTransportCreation } from './sendTransportGuard';
3
4
  import { Socket } from "socket.io-client";
4
5
  import {
5
6
  ConnectSendTransportParameters,
@@ -224,7 +225,7 @@ const createLocalSendTransport = async ({
224
225
  * .catch((error) => console.error('Error creating send transport:', error));
225
226
  */
226
227
 
227
- export const createSendTransport: CreateSendTransportType = async ({
228
+ const createSendTransportNow = async ({
228
229
  option,
229
230
  parameters,
230
231
  }: CreateSendTransportOptions): Promise<void> => {
@@ -262,14 +263,17 @@ export const createSendTransport: CreateSendTransportType = async ({
262
263
  console.log("Error creating local send transport:", error);
263
264
  }
264
265
 
265
- // Emit createWebRtcTransport event to the server
266
- socket.emit(
266
+ // Wait for the acknowledgement and the first producer before resolving, so a second media action never sees a
267
+ // half-created transport (transportCreated true while producerTransport is still null).
268
+ await new Promise<void>((resolve, reject) => socket.emit(
267
269
  "createWebRtcTransport",
268
270
  { consumer: false, islevel: islevel },
269
271
  async ({ params }: { params: any }) => {
272
+ try {
270
273
  // Check if there is an error in the response
271
274
  if (params && params.error) {
272
275
  console.error("Error in createWebRtcTransport:", params.error);
276
+ reject(new Error(String(params.error)));
273
277
  return;
274
278
  }
275
279
 
@@ -278,6 +282,7 @@ export const createSendTransport: CreateSendTransportType = async ({
278
282
  : (device ?? null);
279
283
  if (!resolvedDevice) {
280
284
  // Late acknowledgement after teardown; no transport should survive it.
285
+ resolve();
281
286
  return;
282
287
  }
283
288
 
@@ -342,9 +347,62 @@ export const createSendTransport: CreateSendTransportType = async ({
342
347
  },
343
348
  });
344
349
  updateTransportCreated(transportCreated);
350
+ resolve();
351
+ } catch (error) {
352
+ reject(error);
353
+ }
345
354
  }
346
- );
355
+ ));
356
+
357
+ } catch (error) {
358
+ console.log("Error creating send transport:", error);
359
+ }
360
+ };
361
+
362
+ const producerFor = (params: any, option: CreateSendTransportOptions["option"]): boolean => {
363
+ if (option === "audio") return isOpen(params.audioProducer);
364
+ if (option === "video") return isOpen(params.videoProducer);
365
+ if (option === "screen") return isOpen(params.screenProducer);
366
+ return isOpen(params.audioProducer) && isOpen(params.videoProducer);
367
+ };
347
368
 
369
+ /**
370
+ * Creates the send transport for this socket, or reuses the one that exists or is being created.
371
+ *
372
+ * The server keeps a single producer transport per socket, so a second creation while one is in flight (microphone
373
+ * and camera started together, a rejoin during a toggle) would replace the first on the server and silence the
374
+ * client's uplink. Later callers wait for the creation in flight and then produce their own media on that transport.
375
+ */
376
+ export const createSendTransport: CreateSendTransportType = async ({
377
+ option,
378
+ parameters,
379
+ }: CreateSendTransportOptions): Promise<void> => {
380
+ try {
381
+ const current = (parameters.getCurrentParams?.() ?? parameters.getUpdatedAllParams?.() ?? parameters);
382
+ const socket = current.socket;
383
+ const pending = pendingSendTransportCreation(socket);
384
+
385
+ // Checked and claimed synchronously, so two callers in the same tick cannot both create.
386
+ if (!pending && !(current.transportCreated && isOpen(current.producerTransport))) {
387
+ await trackSendTransportCreation(socket, () => createSendTransportNow({ option, parameters }));
388
+ return;
389
+ }
390
+
391
+ if (pending) await pending.catch(() => undefined);
392
+ const latest = (parameters.getCurrentParams?.() ?? parameters.getUpdatedAllParams?.() ?? parameters);
393
+ if (!isOpen(latest.producerTransport)) {
394
+ // The creation this caller waited for failed or the transport has closed: start again (still one at a time).
395
+ await createSendTransport({ option, parameters });
396
+ return;
397
+ }
398
+ if (producerFor(latest, option)) return; // that media is already being sent
399
+
400
+ const connectSendTransport = latest.connectSendTransport ?? parameters.connectSendTransport;
401
+ await connectSendTransport({
402
+ targetOption: "all",
403
+ option,
404
+ parameters: { ...latest, producerTransport: latest.producerTransport },
405
+ });
348
406
  } catch (error) {
349
407
  console.log("Error creating send transport:", error);
350
408
  }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Send-side serialisation.
3
+ *
4
+ * The media server keeps one producer transport per socket. When two media actions (microphone and camera turned on
5
+ * together, a rejoin while a toggle is in flight) each ask for a send transport, the second `createWebRtcTransport`
6
+ * replaces the first on the server: both client transports then send `transport-connect` to the same server
7
+ * transport, the second fails ("connect() already called") and the client's uplink goes silent. Likewise two
8
+ * concurrent produces of the same kind create two producers where the server accepts one.
9
+ *
10
+ * These helpers make later callers wait for the creation (or produce) already in flight and reuse its result.
11
+ */
12
+
13
+ // Keyed by the socket object: one creation per socket, released as soon as it settles.
14
+ const pendingCreations = new WeakMap<object, Promise<void>>();
15
+
16
+ /** The send-transport creation in flight for this socket, if any. */
17
+ export const pendingSendTransportCreation = (socket: object | null | undefined): Promise<void> | undefined =>
18
+ (socket ? pendingCreations.get(socket) : undefined);
19
+
20
+ /** Runs `create` as the only send-transport creation for this socket until it settles. */
21
+ export const trackSendTransportCreation = (socket: object | null | undefined, create: () => Promise<void>): Promise<void> => {
22
+ if (!socket) return create();
23
+ const running = create();
24
+ pendingCreations.set(socket, running);
25
+ const release = () => {
26
+ if (pendingCreations.get(socket) === running) pendingCreations.delete(socket);
27
+ };
28
+ running.then(release, release);
29
+ return running;
30
+ };
31
+
32
+ interface ClosableLike {
33
+ closed?: boolean;
34
+ }
35
+
36
+ /** True for an existing, open transport or producer. */
37
+ export const isOpen = (item: ClosableLike | null | undefined): boolean => Boolean(item && !item.closed);
38
+
39
+ // Keyed by transport, then by what is produced (audio, video, screen).
40
+ const pendingProduces = new WeakMap<object, Map<string, Promise<unknown>>>();
41
+
42
+ /**
43
+ * Produces once per transport and key at a time. A caller that arrives while the same produce is in flight gets that
44
+ * producer (`joined: true`) instead of creating a second one.
45
+ */
46
+ export const produceOnce = async <T>(
47
+ transport: object,
48
+ key: string,
49
+ produce: () => Promise<T>,
50
+ ): Promise<{ producer: T; joined: boolean }> => {
51
+ let byKey = pendingProduces.get(transport);
52
+ if (!byKey) {
53
+ byKey = new Map();
54
+ pendingProduces.set(transport, byKey);
55
+ }
56
+ const inFlight = byKey.get(key) as Promise<T> | undefined;
57
+ if (inFlight) return { producer: await inFlight, joined: true };
58
+ const running = produce();
59
+ byKey.set(key, running);
60
+ try {
61
+ return { producer: await running, joined: false };
62
+ } finally {
63
+ if (byKey.get(key) === running) byKey.delete(key);
64
+ }
65
+ };
@@ -10,6 +10,33 @@ import {
10
10
  normalizeRejectionReason,
11
11
  } from './mediaSocketErrors';
12
12
 
13
+ /**
14
+ * After the handshake, Socket.IO reconnects this socket on its own whenever the transport drops. A media node that
15
+ * refuses such a reconnect for good (the room ended, the credentials were replaced by a newer join, no meeting
16
+ * minutes) would be retried for as long as the page stays open, so a permanent refusal closes the socket instead.
17
+ * Transient refusals (`api-check-failed`) keep today's behaviour.
18
+ */
19
+ export function stopReconnectingWhenRefused(socket: Socket): void {
20
+ const onRejected = (payload?: { reason?: string; stage?: string }) => {
21
+ const refusal = new MediaSocketConnectError({
22
+ phase: 'rejected',
23
+ reason: normalizeRejectionReason(payload?.reason),
24
+ stage: typeof payload?.stage === 'string' ? payload.stage : undefined,
25
+ elapsedMs: 0,
26
+ });
27
+ if (refusal.retryable) return;
28
+ socket.off('connection-rejected', onRejected);
29
+ console.warn(`MediaSFU media socket: reconnect refused (${refusal.reason}); not retrying.`);
30
+ try {
31
+ // With no active socket left on its manager, Socket.IO stops reconnecting.
32
+ socket.disconnect();
33
+ } catch {
34
+ // Already closed.
35
+ }
36
+ };
37
+ socket.on('connection-rejected', onRejected);
38
+ }
39
+
13
40
  /**
14
41
  * Validates the provided API key or token.
15
42
  * @param {string} value - The API key or token to validate.
@@ -192,7 +219,10 @@ async function connectSocket(
192
219
  }
193
220
 
194
221
  console.log(`Connected to ${conn} socket with ID: ${socketId}`);
195
- settle(() => resolve(socket));
222
+ settle(() => {
223
+ stopReconnectingWhenRefused(socket);
224
+ resolve(socket);
225
+ });
196
226
  };
197
227
 
198
228
  const handleConnectError = (error: Error) => {