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.
- package/CHANGELOG.md +7 -0
- package/dist/consumers/connectSendTransportAudio.d.ts.map +1 -1
- package/dist/consumers/connectSendTransportScreen.d.ts.map +1 -1
- package/dist/consumers/connectSendTransportVideo.d.ts.map +1 -1
- package/dist/consumers/createSendTransport.d.ts +4 -48
- package/dist/consumers/createSendTransport.d.ts.map +1 -1
- package/dist/consumers/index.cjs +1 -1
- package/dist/consumers/index.js +1 -1
- package/dist/consumers/sendTransportGuard.d.ts +30 -0
- package/dist/consumers/sendTransportGuard.d.ts.map +1 -0
- package/dist/{getParticipantMedia-DEJwXXhU.js → getParticipantMedia-CZbsFBth.js} +3 -3
- package/dist/{getParticipantMedia-DEJwXXhU.js.map → getParticipantMedia-CZbsFBth.js.map} +1 -1
- package/dist/{getParticipantMedia-1okyF4H7.cjs → getParticipantMedia-Dli4dOLp.cjs} +3 -3
- package/dist/{getParticipantMedia-1okyF4H7.cjs.map → getParticipantMedia-Dli4dOLp.cjs.map} +1 -1
- package/dist/index.cjs +4 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +9 -8
- package/dist/index.native.cjs +4 -3
- package/dist/index.native.cjs.map +1 -1
- package/dist/index.native.js +8 -7
- package/dist/{joinLocalRoom-DS6_6lJL.js → joinLocalRoom-BWoS-8wt.js} +2 -2
- package/dist/{joinLocalRoom-DS6_6lJL.js.map → joinLocalRoom-BWoS-8wt.js.map} +1 -1
- package/dist/{joinLocalRoom-Dsn39SwS.cjs → joinLocalRoom-DQvud99t.cjs} +2 -2
- package/dist/{joinLocalRoom-Dsn39SwS.cjs.map → joinLocalRoom-DQvud99t.cjs.map} +1 -1
- package/dist/sockets/SocketManager.d.ts +7 -0
- package/dist/sockets/SocketManager.d.ts.map +1 -1
- package/dist/types/index.cjs +2 -2
- package/dist/types/index.js +2 -2
- package/dist/{updateParticipantAudioDecibels-Dni8i6AY.cjs → updateParticipantAudioDecibels-CbCP1PDs.cjs} +162 -67
- package/dist/updateParticipantAudioDecibels-CbCP1PDs.cjs.map +1 -0
- package/dist/{updateParticipantAudioDecibels-BBfdHWQ0.js → updateParticipantAudioDecibels-xKKq7Zp5.js} +166 -71
- package/dist/updateParticipantAudioDecibels-xKKq7Zp5.js.map +1 -0
- package/package.json +1 -1
- package/src/consumers/connectSendTransportAudio.ts +7 -4
- package/src/consumers/connectSendTransportScreen.ts +6 -4
- package/src/consumers/connectSendTransportVideo.ts +4 -2
- package/src/consumers/createSendTransport.ts +62 -4
- package/src/consumers/sendTransportGuard.ts +65 -0
- package/src/sockets/SocketManager.ts +31 -1
- package/dist/updateParticipantAudioDecibels-BBfdHWQ0.js.map +0 -1
- 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.
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
266
|
-
|
|
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(() =>
|
|
222
|
+
settle(() => {
|
|
223
|
+
stopReconnectingWhenRefused(socket);
|
|
224
|
+
resolve(socket);
|
|
225
|
+
});
|
|
196
226
|
};
|
|
197
227
|
|
|
198
228
|
const handleConnectError = (error: Error) => {
|