mediasfu-shared 1.2.7 → 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 +17 -0
- package/dist/ProducerClient/producerClientEmits/updateRoomParametersClient.d.ts +2 -0
- package/dist/ProducerClient/producerClientEmits/updateRoomParametersClient.d.ts.map +1 -1
- package/dist/audioProcessing-Ca5wm1nV.js +150 -0
- package/dist/audioProcessing-Ca5wm1nV.js.map +1 -0
- package/dist/audioProcessing-VCbu70Jt.cjs +149 -0
- package/dist/audioProcessing-VCbu70Jt.cjs.map +1 -0
- package/dist/consumers/closeAndResize.d.ts +3 -0
- package/dist/consumers/closeAndResize.d.ts.map +1 -1
- package/dist/consumers/connectIps.d.ts +6 -0
- package/dist/consumers/connectIps.d.ts.map +1 -1
- package/dist/consumers/connectSendTransportAudio.d.ts.map +1 -1
- package/dist/consumers/connectSendTransportScreen.d.ts.map +1 -1
- package/dist/consumers/connectSendTransportScreenAudio.d.ts +65 -0
- package/dist/consumers/connectSendTransportScreenAudio.d.ts.map +1 -0
- package/dist/consumers/connectSendTransportVideo.d.ts.map +1 -1
- package/dist/consumers/consumerBackupPreference.d.ts +14 -0
- package/dist/consumers/consumerBackupPreference.d.ts.map +1 -0
- package/dist/consumers/createSendTransport.d.ts +4 -48
- package/dist/consumers/createSendTransport.d.ts.map +1 -1
- package/dist/consumers/disconnectSendTransportScreen.d.ts.map +1 -1
- package/dist/consumers/getPipedProducersAlt.d.ts.map +1 -1
- package/dist/consumers/index.cjs +23 -1
- package/dist/consumers/index.cjs.map +1 -1
- package/dist/consumers/index.d.ts +3 -0
- package/dist/consumers/index.d.ts.map +1 -1
- package/dist/consumers/index.js +61 -39
- package/dist/consumers/index.js.map +1 -1
- package/dist/consumers/screenAudio.d.ts +64 -0
- package/dist/consumers/screenAudio.d.ts.map +1 -0
- package/dist/consumers/sendTransportGuard.d.ts +30 -0
- package/dist/consumers/sendTransportGuard.d.ts.map +1 -0
- package/dist/consumers/socketReceiveMethods/producerClosed.d.ts.map +1 -1
- package/dist/consumers/startShareScreen.d.ts +14 -0
- package/dist/consumers/startShareScreen.d.ts.map +1 -1
- package/dist/consumers/streamSuccessScreen.d.ts.map +1 -1
- package/dist/{getParticipantMedia-DTZg8n5U.js → getParticipantMedia-CZbsFBth.js} +4 -4
- package/dist/{getParticipantMedia-DTZg8n5U.js.map → getParticipantMedia-CZbsFBth.js.map} +1 -1
- package/dist/{getParticipantMedia-c2bbdT5f.cjs → getParticipantMedia-Dli4dOLp.cjs} +4 -4
- package/dist/{getParticipantMedia-c2bbdT5f.cjs.map → getParticipantMedia-Dli4dOLp.cjs.map} +1 -1
- package/dist/index.cjs +37 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1525 -1494
- package/dist/index.native.cjs +37 -6
- package/dist/index.native.cjs.map +1 -1
- package/dist/index.native.d.ts +4 -0
- package/dist/index.native.d.ts.map +1 -1
- package/dist/index.native.js +107 -76
- package/dist/{joinLocalRoom-1dRd2PCN.js → joinLocalRoom-BWoS-8wt.js} +6 -4
- package/dist/joinLocalRoom-BWoS-8wt.js.map +1 -0
- package/dist/{joinLocalRoom-DwY-3ZsH.cjs → joinLocalRoom-DQvud99t.cjs} +6 -4
- package/dist/joinLocalRoom-DQvud99t.cjs.map +1 -0
- package/dist/{joinRoomOnMediaSFU-BnAWLciF.js → joinRoomOnMediaSFU-B6tTA0Mx.js} +17 -13
- package/dist/joinRoomOnMediaSFU-B6tTA0Mx.js.map +1 -0
- package/dist/{joinRoomOnMediaSFU-D2C7q1GK.cjs → joinRoomOnMediaSFU-CxwD9fBz.cjs} +17 -13
- package/dist/joinRoomOnMediaSFU-CxwD9fBz.cjs.map +1 -0
- package/dist/methods/index.cjs +11 -8
- package/dist/methods/index.cjs.map +1 -1
- package/dist/methods/index.js +13 -9
- package/dist/methods/index.js.map +1 -1
- package/dist/methods/socketReceive/getDomains.d.ts.map +1 -1
- package/dist/methods/utils/checkLimitsAndMakeRequest.d.ts.map +1 -1
- package/dist/methods/welcome/handleWelcomeRequest.d.ts.map +1 -1
- package/dist/sockets/SocketManager.d.ts +7 -0
- package/dist/sockets/SocketManager.d.ts.map +1 -1
- package/dist/sockets/mediaSocketErrors.d.ts +62 -0
- package/dist/sockets/mediaSocketErrors.d.ts.map +1 -0
- package/dist/types/index.cjs +4 -3
- package/dist/types/index.cjs.map +1 -1
- package/dist/types/index.js +39 -38
- package/dist/types/shared-base-types.d.ts +4 -0
- package/dist/types/shared-base-types.d.ts.map +1 -1
- package/dist/{updateParticipantAudioDecibels-BJ2PxJJD.cjs → updateParticipantAudioDecibels-CbCP1PDs.cjs} +467 -89
- package/dist/updateParticipantAudioDecibels-CbCP1PDs.cjs.map +1 -0
- package/dist/{updateParticipantAudioDecibels-DiJ_LFfH.js → updateParticipantAudioDecibels-xKKq7Zp5.js} +505 -127
- package/dist/updateParticipantAudioDecibels-xKKq7Zp5.js.map +1 -0
- package/package.json +3 -2
- package/src/ProducerClient/producerClientEmits/updateRoomParametersClient.ts +4 -0
- package/src/consumers/closeAndResize.ts +14 -0
- package/src/consumers/connectIps.ts +81 -8
- package/src/consumers/connectSendTransportAudio.ts +7 -4
- package/src/consumers/connectSendTransportScreen.ts +6 -4
- package/src/consumers/connectSendTransportScreenAudio.ts +184 -0
- package/src/consumers/connectSendTransportVideo.ts +4 -2
- package/src/consumers/consumerBackupPreference.ts +43 -0
- package/src/consumers/createSendTransport.ts +62 -4
- package/src/consumers/disconnectSendTransportScreen.ts +8 -0
- package/src/consumers/getPipedProducersAlt.ts +4 -0
- package/src/consumers/index.ts +3 -0
- package/src/consumers/screenAudio.ts +132 -0
- package/src/consumers/sendTransportGuard.ts +65 -0
- package/src/consumers/socketReceiveMethods/producerClosed.ts +3 -0
- package/src/consumers/startShareScreen.ts +44 -8
- package/src/consumers/streamSuccessScreen.ts +11 -0
- package/src/index.native.ts +4 -0
- package/src/index.ts +4 -0
- package/src/methods/socketReceive/getDomains.ts +4 -0
- package/src/methods/utils/checkLimitsAndMakeRequest.ts +24 -11
- package/src/methods/welcome/handleWelcomeRequest.ts +15 -6
- package/src/sockets/SocketManager.ts +107 -13
- package/src/sockets/mediaSocketErrors.ts +156 -0
- package/src/types/shared-base-types.ts +4 -0
- package/dist/audioProcessing-0z5KlcmO.js +0 -35
- package/dist/audioProcessing-0z5KlcmO.js.map +0 -1
- package/dist/audioProcessing-BpbrdCk-.cjs +0 -34
- package/dist/audioProcessing-BpbrdCk-.cjs.map +0 -1
- package/dist/joinLocalRoom-1dRd2PCN.js.map +0 -1
- package/dist/joinLocalRoom-DwY-3ZsH.cjs.map +0 -1
- package/dist/joinRoomOnMediaSFU-BnAWLciF.js.map +0 -1
- package/dist/joinRoomOnMediaSFU-D2C7q1GK.cjs.map +0 -1
- package/dist/updateParticipantAudioDecibels-BJ2PxJJD.cjs.map +0 -1
- package/dist/updateParticipantAudioDecibels-DiJ_LFfH.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mediasfu-shared",
|
|
3
|
-
"version": "1.
|
|
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",
|
|
@@ -45,7 +45,8 @@
|
|
|
45
45
|
"build-docs": "typedoc",
|
|
46
46
|
"dev": "vite build --watch",
|
|
47
47
|
"test:staging:smoke": "npm run build && node ./scripts/staging-room-smoke-test.cjs",
|
|
48
|
-
"type-check": "tsc --noEmit"
|
|
48
|
+
"type-check": "tsc --noEmit",
|
|
49
|
+
"test:sdk-parity": "node tests/sdkParity.test.cjs"
|
|
49
50
|
},
|
|
50
51
|
"keywords": [
|
|
51
52
|
"mediasfu",
|
|
@@ -117,6 +117,8 @@ export interface UpdateRoomParametersClientParameters {
|
|
|
117
117
|
updateRecordingPreferredOrientation: (orientation: string) => void;
|
|
118
118
|
updateRecordingSupportForOtherOrientation: (support: boolean) => void;
|
|
119
119
|
updateRecordingMultiFormatsSupport: (support: boolean) => void;
|
|
120
|
+
/** Records whether the media server accepts screen-share audio (join response `supportsScreenAudio`). */
|
|
121
|
+
updateSupportsScreenAudio?: (supported: boolean) => void;
|
|
120
122
|
updateRecordingVideoOptions: (options: string) => void;
|
|
121
123
|
updateRecordingAudioOptions: (options: string) => void;
|
|
122
124
|
}
|
|
@@ -184,6 +186,7 @@ export const updateRoomParametersClient = ({ parameters, simulcastOff = paramete
|
|
|
184
186
|
updateRecordingPreferredOrientation,
|
|
185
187
|
updateRecordingSupportForOtherOrientation,
|
|
186
188
|
updateRecordingMultiFormatsSupport,
|
|
189
|
+
updateSupportsScreenAudio,
|
|
187
190
|
updateRecordingVideoOptions,
|
|
188
191
|
updateRecordingAudioOptions,
|
|
189
192
|
updateMainHeightWidth,
|
|
@@ -221,6 +224,7 @@ export const updateRoomParametersClient = ({ parameters, simulcastOff = paramete
|
|
|
221
224
|
updateRecordingPreferredOrientation(data.recordingParams.recordingPreferredOrientation);
|
|
222
225
|
updateRecordingSupportForOtherOrientation(data.recordingParams.recordingSupportForOtherOrientation);
|
|
223
226
|
updateRecordingMultiFormatsSupport(data.recordingParams.recordingMultiFormatsSupport);
|
|
227
|
+
updateSupportsScreenAudio?.(data.supportsScreenAudio === true);
|
|
224
228
|
|
|
225
229
|
updateItemPageLimit(data.meetingRoomParams.itemPageLimit);
|
|
226
230
|
updateEventType(data.meetingRoomParams.type);
|
|
@@ -8,6 +8,8 @@ import {
|
|
|
8
8
|
|
|
9
9
|
export interface CloseAndResizeParameters extends ReorderStreamsParameters, PrepopulateUserMediaParameters, RePortParameters {
|
|
10
10
|
allAudioStreams: (Stream | Participant)[];
|
|
11
|
+
/** Rendered audio-only players (React elements, Vue/Angular component descriptors). */
|
|
12
|
+
audioOnlyStreams?: any[];
|
|
11
13
|
allVideoStreams: (Stream | Participant)[];
|
|
12
14
|
activeNames: string[];
|
|
13
15
|
participants: Participant[];
|
|
@@ -32,6 +34,7 @@ export interface CloseAndResizeParameters extends ReorderStreamsParameters, Prep
|
|
|
32
34
|
updateMainWindow: boolean;
|
|
33
35
|
updateActiveNames: (activeNames: string[]) => void;
|
|
34
36
|
updateAllAudioStreams: (allAudioStreams: (Stream | Participant)[]) => void;
|
|
37
|
+
updateAudioOnlyStreams?: (audioOnlyStreams: any[]) => void;
|
|
35
38
|
updateShareScreenStarted: (shareScreenStarted: boolean) => void;
|
|
36
39
|
updateUpdateMainWindow: (updateMainWindow: boolean) => void;
|
|
37
40
|
updateNewLimitedStreams: (newLimitedStreams: (Stream | Participant)[]) => void;
|
|
@@ -176,6 +179,7 @@ export const closeAndResize = async ({ producerId, kind, parameters }: CloseAndR
|
|
|
176
179
|
|
|
177
180
|
let {
|
|
178
181
|
allAudioStreams,
|
|
182
|
+
audioOnlyStreams = [],
|
|
179
183
|
allVideoStreams,
|
|
180
184
|
activeNames,
|
|
181
185
|
participants,
|
|
@@ -201,6 +205,7 @@ export const closeAndResize = async ({ producerId, kind, parameters }: CloseAndR
|
|
|
201
205
|
updateMainWindow,
|
|
202
206
|
updateActiveNames,
|
|
203
207
|
updateAllAudioStreams,
|
|
208
|
+
updateAudioOnlyStreams,
|
|
204
209
|
updateAllVideoStreams,
|
|
205
210
|
|
|
206
211
|
updateShareScreenStarted,
|
|
@@ -235,6 +240,15 @@ export const closeAndResize = async ({ producerId, kind, parameters }: CloseAndR
|
|
|
235
240
|
|
|
236
241
|
updateAllAudioStreams(allAudioStreams);
|
|
237
242
|
|
|
243
|
+
// Audio-only players (mic and screen audio) are stored separately from allAudioStreams. Leaving one mounted after
|
|
244
|
+
// its producer closes means a rejoin adds a second player, heard as echo or duplicate speech.
|
|
245
|
+
const playerProducerId = (element: any): string =>
|
|
246
|
+
String(element?.props?.remoteProducerId ?? element?.inputs?.remoteProducerId ?? element?.key ?? '');
|
|
247
|
+
if (audioOnlyStreams.some((element: any) => playerProducerId(element) === producerId)) {
|
|
248
|
+
audioOnlyStreams = audioOnlyStreams.filter((element: any) => playerProducerId(element) !== producerId);
|
|
249
|
+
updateAudioOnlyStreams?.(audioOnlyStreams);
|
|
250
|
+
}
|
|
251
|
+
|
|
238
252
|
if (recordingDisplayType == "video" && recordingVideoOptimized == true) {
|
|
239
253
|
// optimize the video display
|
|
240
254
|
} else {
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
|
|
2
|
-
import { connectSocket } from "
|
|
2
|
+
import { connectSocket } from "../sockets/SocketManager";
|
|
3
3
|
import { newPipeProducer } from "./socketReceiveMethods/newPipeProducer";
|
|
4
4
|
import { producerClosed } from "./socketReceiveMethods/producerClosed";
|
|
5
5
|
import { joinConsumeRoom } from "./socketReceiveMethods/joinConsumeRoom";
|
|
6
|
+
import { markScreenAudioProducer } from "./screenAudio";
|
|
7
|
+
import { consumerBackupPreferDelay } from "./consumerBackupPreference";
|
|
8
|
+
import { isMediaSocketConnectError } from "../sockets/mediaSocketErrors";
|
|
6
9
|
import type { Device } from 'mediasoup-client/types';
|
|
7
10
|
import {
|
|
8
11
|
ReorderStreamsParameters, ReorderStreamsType, NewPipeProducerParameters, NewPipeProducerType, ProducerClosedType,
|
|
@@ -36,9 +39,9 @@ export interface ConnectIpsOptions {
|
|
|
36
39
|
// Export the type definition for the function
|
|
37
40
|
export type ConnectIpsType = (options: ConnectIpsOptions) => Promise<[Record<string, any>[], string[]]>;
|
|
38
41
|
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
+
// Scope pending connections to the caller's socket collection. This prevents
|
|
43
|
+
// overlapping room/domain updates from opening the same consume endpoint more
|
|
44
|
+
// than once without sharing sockets between independent room engines.
|
|
42
45
|
const pendingConsumeConnections = new WeakMap<
|
|
43
46
|
object,
|
|
44
47
|
Map<string, Promise<boolean>>
|
|
@@ -47,6 +50,28 @@ const pendingConsumeConnections = new WeakMap<
|
|
|
47
50
|
const normalizeConsumeEndpoint = (ip: string): string =>
|
|
48
51
|
ip.trim().toLowerCase().replace(/\.$/, "");
|
|
49
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Consume endpoints that refused these credentials (`connection-rejected` with a permanent reason). Domain updates call
|
|
55
|
+
* connectIps again and again; without this every update would retry the same refusal. Keyed by the caller's socket
|
|
56
|
+
* collection so another room (new credentials) starts clean.
|
|
57
|
+
*/
|
|
58
|
+
export const CONSUME_REJECTION_COOLDOWN_MS = 60000;
|
|
59
|
+
const rejectedConsumeEndpoints = new WeakMap<object, Map<string, number>>();
|
|
60
|
+
|
|
61
|
+
const consumeEndpointRecentlyRejected = (owner: object, endpoint: string): boolean => {
|
|
62
|
+
const at = rejectedConsumeEndpoints.get(owner)?.get(endpoint);
|
|
63
|
+
return at !== undefined && Date.now() - at < CONSUME_REJECTION_COOLDOWN_MS;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const rememberConsumeEndpointRejection = (owner: object, endpoint: string): void => {
|
|
67
|
+
let byEndpoint = rejectedConsumeEndpoints.get(owner);
|
|
68
|
+
if (!byEndpoint) {
|
|
69
|
+
byEndpoint = new Map<string, number>();
|
|
70
|
+
rejectedConsumeEndpoints.set(owner, byEndpoint);
|
|
71
|
+
}
|
|
72
|
+
byEndpoint.set(endpoint, Date.now());
|
|
73
|
+
};
|
|
74
|
+
|
|
50
75
|
const hasConsumeEndpoint = (
|
|
51
76
|
consumeSockets: ConsumeSocket[],
|
|
52
77
|
endpoint: string
|
|
@@ -59,6 +84,7 @@ const hasConsumeEndpoint = (
|
|
|
59
84
|
const socket = socketObj[ip];
|
|
60
85
|
if (socket?.connected && socket.id) return true;
|
|
61
86
|
|
|
87
|
+
// A dead entry must not suppress the next room's connection attempt.
|
|
62
88
|
socket?.removeAllListeners?.();
|
|
63
89
|
socket?.disconnect?.();
|
|
64
90
|
consumeSockets.splice(index, 1);
|
|
@@ -176,7 +202,9 @@ export const connectIps = async ({
|
|
|
176
202
|
return [consume_sockets, roomRecvIPs];
|
|
177
203
|
}
|
|
178
204
|
|
|
205
|
+
const rejectionOwner: object = parameters?.socket || consume_sockets;
|
|
179
206
|
for (const ip of remIP) {
|
|
207
|
+
if (consumeEndpointRecentlyRejected(rejectionOwner, normalizeConsumeEndpoint(ip))) continue;
|
|
180
208
|
const releaseReservation = await reserveConsumeEndpoint(
|
|
181
209
|
consume_sockets,
|
|
182
210
|
ip,
|
|
@@ -191,6 +219,20 @@ export const connectIps = async ({
|
|
|
191
219
|
remote_sock = await connectSocket({ apiUserName, apiKey, apiToken, link: `https://${ip}.mediasfu.com` });
|
|
192
220
|
|
|
193
221
|
if (remote_sock.id) {
|
|
222
|
+
let consumeRoomReady = false;
|
|
223
|
+
const pendingProducers: Array<{
|
|
224
|
+
producerId: string;
|
|
225
|
+
islevel: string;
|
|
226
|
+
isTranslation?: boolean;
|
|
227
|
+
translationMeta?: {
|
|
228
|
+
speakerId: string;
|
|
229
|
+
speakerName: string;
|
|
230
|
+
language: string;
|
|
231
|
+
originalProducerId?: string;
|
|
232
|
+
isSpeakerControlled?: boolean;
|
|
233
|
+
};
|
|
234
|
+
}> = [];
|
|
235
|
+
|
|
194
236
|
// Check if the IP is in the roomRecvIPs, if not, add it
|
|
195
237
|
if (!roomRecvIPs.includes(ip)) {
|
|
196
238
|
roomRecvIPs.push(ip);
|
|
@@ -198,7 +240,21 @@ export const connectIps = async ({
|
|
|
198
240
|
}
|
|
199
241
|
|
|
200
242
|
// Handle new pipe producer event
|
|
201
|
-
|
|
243
|
+
const consumeProducer = async ({ producerId, islevel, isTranslation, translationMeta, mediaTag }: { producerId: string; islevel: string; isTranslation?: boolean; translationMeta?: { speakerId: string; speakerName: string; language: string; originalProducerId?: string; isSpeakerControlled?: boolean }; mediaTag?: string }) => {
|
|
244
|
+
// Screen-share audio is tagged by the server; remember it before the consumer resumes.
|
|
245
|
+
markScreenAudioProducer(producerId, mediaTag);
|
|
246
|
+
if (!consumeRoomReady) {
|
|
247
|
+
pendingProducers.push({ producerId, islevel, isTranslation, translationMeta });
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
// A busy node with a connected backup: give the backup's announcement a moment to win (same producer id).
|
|
251
|
+
const preferDelay = isTranslation ? 0 : consumerBackupPreferDelay(
|
|
252
|
+
ip,
|
|
253
|
+
(parameters.getUpdatedAllParams?.() as any)?.consume_sockets as any
|
|
254
|
+
);
|
|
255
|
+
if (preferDelay > 0) {
|
|
256
|
+
await new Promise((resolve) => setTimeout(resolve, preferDelay));
|
|
257
|
+
}
|
|
202
258
|
if (newProducerMethod) {
|
|
203
259
|
await newProducerMethod({
|
|
204
260
|
producerId,
|
|
@@ -209,7 +265,8 @@ export const connectIps = async ({
|
|
|
209
265
|
translationMeta,
|
|
210
266
|
});
|
|
211
267
|
}
|
|
212
|
-
}
|
|
268
|
+
};
|
|
269
|
+
remote_sock.on("new-pipe-producer", consumeProducer);
|
|
213
270
|
|
|
214
271
|
// Handle producer closed event
|
|
215
272
|
remote_sock.on("producer-closed", async ({ remoteProducerId }: { remoteProducerId: string }) => {
|
|
@@ -231,6 +288,18 @@ export const connectIps = async ({
|
|
|
231
288
|
}
|
|
232
289
|
}
|
|
233
290
|
|
|
291
|
+
// Events can arrive after the socket connects but before it has
|
|
292
|
+
// joined the consuming room. Emitting transport requests during that
|
|
293
|
+
// window receives no acknowledgement and poisons global de-duplication.
|
|
294
|
+
// The join's producer sweep covers current producers; flushing the
|
|
295
|
+
// queue closes the event/sweep gap, and signal-level de-duplication
|
|
296
|
+
// makes overlap harmless.
|
|
297
|
+
const queuedProducers = pendingProducers.splice(0);
|
|
298
|
+
consumeRoomReady = true;
|
|
299
|
+
for (const producer of queuedProducers) {
|
|
300
|
+
await consumeProducer(producer);
|
|
301
|
+
}
|
|
302
|
+
|
|
234
303
|
// Add the remote socket to the consume_sockets array
|
|
235
304
|
consume_sockets.push({ [ip]: remote_sock });
|
|
236
305
|
updateConsume_sockets(consume_sockets);
|
|
@@ -256,8 +325,12 @@ export const connectIps = async ({
|
|
|
256
325
|
});
|
|
257
326
|
}
|
|
258
327
|
} catch (error) {
|
|
259
|
-
|
|
260
|
-
|
|
328
|
+
if (isMediaSocketConnectError(error) && !error.retryable) {
|
|
329
|
+
rememberConsumeEndpointRejection(rejectionOwner, normalizeConsumeEndpoint(ip));
|
|
330
|
+
console.warn(`connectIps: ${ip} refused this room (${error.reason}); not retrying for ${CONSUME_REJECTION_COOLDOWN_MS / 1000} s.`, error.message);
|
|
331
|
+
} else {
|
|
332
|
+
console.log("connectIps error", error);
|
|
333
|
+
}
|
|
261
334
|
} finally {
|
|
262
335
|
if (!connected && remote_sock) {
|
|
263
336
|
remote_sock.removeAllListeners?.();
|
|
@@ -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);
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import type { Producer, Transport } from "mediasoup-client/types";
|
|
2
|
+
import { Socket } from "socket.io-client";
|
|
3
|
+
import {
|
|
4
|
+
SCREEN_AUDIO_CODEC_OPTIONS,
|
|
5
|
+
SCREEN_AUDIO_MEDIA_TAG,
|
|
6
|
+
recallScreenAudioProducer,
|
|
7
|
+
rememberScreenAudioProducer,
|
|
8
|
+
shouldShareScreenAudio,
|
|
9
|
+
} from "./screenAudio";
|
|
10
|
+
|
|
11
|
+
export interface ScreenAudioParameters {
|
|
12
|
+
socket: Socket;
|
|
13
|
+
producerTransport: Transport | null;
|
|
14
|
+
screenProducer?: Producer | null;
|
|
15
|
+
screenAudioProducer?: Producer | null;
|
|
16
|
+
supportsScreenAudio?: boolean;
|
|
17
|
+
screenAudioEnabled?: boolean;
|
|
18
|
+
updateScreenAudioProducer?: (producer: Producer | null) => void;
|
|
19
|
+
getUpdatedAllParams: () => ScreenAudioParameters;
|
|
20
|
+
[key: string]: any;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ConnectSendTransportScreenAudioOptions {
|
|
24
|
+
stream: MediaStream;
|
|
25
|
+
parameters: ScreenAudioParameters;
|
|
26
|
+
/** Wait before the single retry when the server has not registered the screen share yet. */
|
|
27
|
+
retryDelayMs?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type ConnectSendTransportScreenAudioType = (
|
|
31
|
+
options: ConnectSendTransportScreenAudioOptions
|
|
32
|
+
) => Promise<Producer | null>;
|
|
33
|
+
|
|
34
|
+
export interface DisconnectSendTransportScreenAudioOptions {
|
|
35
|
+
parameters: ScreenAudioParameters;
|
|
36
|
+
/** Also tell the server (the screen share continues). Not needed when the whole screen share stops. */
|
|
37
|
+
notifyServer?: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type DisconnectSendTransportScreenAudioType = (
|
|
41
|
+
options: DisconnectSendTransportScreenAudioOptions
|
|
42
|
+
) => Promise<void>;
|
|
43
|
+
|
|
44
|
+
const currentScreenAudioProducer = (parameters: ScreenAudioParameters): Producer | null =>
|
|
45
|
+
parameters.screenAudioProducer || recallScreenAudioProducer(parameters.socket);
|
|
46
|
+
|
|
47
|
+
const storeScreenAudioProducer = (parameters: ScreenAudioParameters, producer: Producer | null) => {
|
|
48
|
+
rememberScreenAudioProducer(parameters.socket, producer);
|
|
49
|
+
parameters.updateScreenAudioProducer?.(producer);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Closes the screen-audio producer, if any. Safe to call when none exists.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```typescript
|
|
57
|
+
* await disconnectSendTransportScreenAudio({ parameters, notifyServer: true });
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export const disconnectSendTransportScreenAudio: DisconnectSendTransportScreenAudioType = async ({
|
|
61
|
+
parameters,
|
|
62
|
+
notifyServer = false,
|
|
63
|
+
}) => {
|
|
64
|
+
const params = parameters.getUpdatedAllParams ? parameters.getUpdatedAllParams() : parameters;
|
|
65
|
+
const producer = currentScreenAudioProducer(params);
|
|
66
|
+
if (!producer) return;
|
|
67
|
+
try {
|
|
68
|
+
if (!producer.closed) producer.close();
|
|
69
|
+
} catch {
|
|
70
|
+
// already closed
|
|
71
|
+
}
|
|
72
|
+
storeScreenAudioProducer(params, null);
|
|
73
|
+
if (notifyServer) {
|
|
74
|
+
try {
|
|
75
|
+
params.socket?.emit("closeScreenAudioProducer");
|
|
76
|
+
} catch {
|
|
77
|
+
// the server also closes it with the screen share
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export interface ToggleScreenAudioOptions {
|
|
83
|
+
parameters: ScreenAudioParameters;
|
|
84
|
+
/** Force a state; omitted = toggle. */
|
|
85
|
+
paused?: boolean;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Mutes or unmutes the sharer's screen audio without stopping the screen share (listeners hear silence while muted).
|
|
90
|
+
* Returns the new muted state, or null when no screen audio is being shared.
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```typescript
|
|
94
|
+
* const muted = toggleScreenAudio({ parameters });
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
export const toggleScreenAudio = ({ parameters, paused }: ToggleScreenAudioOptions): boolean | null => {
|
|
98
|
+
const params = parameters.getUpdatedAllParams ? parameters.getUpdatedAllParams() : parameters;
|
|
99
|
+
const producer = currentScreenAudioProducer(params);
|
|
100
|
+
if (!producer || producer.closed) return null;
|
|
101
|
+
const next = paused ?? !producer.paused;
|
|
102
|
+
if (next && !producer.paused) producer.pause();
|
|
103
|
+
if (!next && producer.paused) producer.resume();
|
|
104
|
+
params.updateScreenAudioProducer?.(producer);
|
|
105
|
+
return producer.paused;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const produceOnce = async (transport: Transport, track: MediaStreamTrack): Promise<Producer | null> => {
|
|
109
|
+
const producer = await transport.produce({
|
|
110
|
+
track,
|
|
111
|
+
codecOptions: SCREEN_AUDIO_CODEC_OPTIONS,
|
|
112
|
+
appData: { mediaTag: SCREEN_AUDIO_MEDIA_TAG },
|
|
113
|
+
// The track belongs to the screen share stream, which stops it with the share.
|
|
114
|
+
stopTracks: false,
|
|
115
|
+
});
|
|
116
|
+
// A rejected produce resolves without a server id: drop the local sender.
|
|
117
|
+
if (!producer?.id) {
|
|
118
|
+
try { producer?.close(); } catch { /* nothing to close */ }
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
return producer;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Publishes the audio track of a screen-share stream as a "screen-audio" producer on the existing send transport.
|
|
126
|
+
* Call it after the screen video producer exists. It returns null (and the screen share continues without audio)
|
|
127
|
+
* when the stream has no audio, the server does not support screen audio, or the app turned it off.
|
|
128
|
+
*
|
|
129
|
+
* The producer mirrors the screen producer: it pauses, resumes and closes with it, and it closes alone (telling the
|
|
130
|
+
* server) when its own track ends.
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* ```typescript
|
|
134
|
+
* const producer = await connectSendTransportScreenAudio({ stream: screenStream, parameters });
|
|
135
|
+
* ```
|
|
136
|
+
*/
|
|
137
|
+
export const connectSendTransportScreenAudio: ConnectSendTransportScreenAudioType = async ({
|
|
138
|
+
stream,
|
|
139
|
+
parameters,
|
|
140
|
+
retryDelayMs = 600,
|
|
141
|
+
}) => {
|
|
142
|
+
const params = parameters.getUpdatedAllParams ? parameters.getUpdatedAllParams() : parameters;
|
|
143
|
+
const track = stream?.getAudioTracks?.()[0];
|
|
144
|
+
if (!track || track.readyState === "ended") return null;
|
|
145
|
+
if (!shouldShareScreenAudio(params)) return null;
|
|
146
|
+
const transport = params.producerTransport;
|
|
147
|
+
if (!transport || transport.closed) return null;
|
|
148
|
+
|
|
149
|
+
const existing = currentScreenAudioProducer(params);
|
|
150
|
+
if (existing && !existing.closed) return existing;
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
let producer = await produceOnce(transport, track);
|
|
154
|
+
// The track may have ended while the first attempt was in flight.
|
|
155
|
+
const trackEnded = () => (track.readyState as MediaStreamTrackState) === "ended";
|
|
156
|
+
if (!producer && retryDelayMs > 0 && !trackEnded()) {
|
|
157
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
|
|
158
|
+
producer = await produceOnce(transport, track);
|
|
159
|
+
}
|
|
160
|
+
if (!producer) return null;
|
|
161
|
+
|
|
162
|
+
storeScreenAudioProducer(params, producer);
|
|
163
|
+
|
|
164
|
+
const screenProducer = params.screenProducer;
|
|
165
|
+
if (screenProducer?.paused) producer.pause();
|
|
166
|
+
screenProducer?.observer?.on("pause", () => {
|
|
167
|
+
if (!producer!.closed) producer!.pause();
|
|
168
|
+
});
|
|
169
|
+
screenProducer?.observer?.on("resume", () => {
|
|
170
|
+
if (!producer!.closed) producer!.resume();
|
|
171
|
+
});
|
|
172
|
+
screenProducer?.observer?.on("close", () => {
|
|
173
|
+
void disconnectSendTransportScreenAudio({ parameters: params });
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
track.addEventListener("ended", () => {
|
|
177
|
+
void disconnectSendTransportScreenAudio({ parameters: params, notifyServer: true });
|
|
178
|
+
});
|
|
179
|
+
return producer;
|
|
180
|
+
} catch (error) {
|
|
181
|
+
console.log("connectSendTransportScreenAudio error", error);
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
};
|
|
@@ -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") {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consumer backup preference (server feature consumer-backup-v1).
|
|
3
|
+
*
|
|
4
|
+
* When a consuming node gets busy, the server attaches a backup node and tells every listener
|
|
5
|
+
* `alt_domains: { <busy node>: <backup node> }`. Listeners stay on the busy node for what they already receive, but a
|
|
6
|
+
* stream announced after that is taken from the backup: when the busy node announces it and the backup is connected,
|
|
7
|
+
* the client waits briefly for the backup's own announcement (the backup gets each new stream a moment later) and only
|
|
8
|
+
* falls back to the busy node if it does not come. Consumption is de-duplicated by producer id, so whichever node is
|
|
9
|
+
* used first wins and the other announcement is ignored.
|
|
10
|
+
*
|
|
11
|
+
* The request is never sent to the backup for a stream the backup has not announced: a consume the backup cannot serve
|
|
12
|
+
* fails without an error and the de-duplication would then skip the busy node's copy (staging test, Oct 9 2026).
|
|
13
|
+
*/
|
|
14
|
+
const backupByParent = new Map<string, string>();
|
|
15
|
+
|
|
16
|
+
/** How long a busy node's announcement waits for the backup's (ms). */
|
|
17
|
+
export const CONSUMER_BACKUP_PREFER_DELAY_MS = 1500;
|
|
18
|
+
|
|
19
|
+
/** Records the server's `{ busy node: backup node }` map (from `updateConsumingDomains` / `getDomains`). */
|
|
20
|
+
export const rememberConsumerBackups = (altDomains?: Record<string, string> | null): void => {
|
|
21
|
+
for (const [parent, backup] of Object.entries(altDomains || {})) {
|
|
22
|
+
if (parent && backup && parent !== backup) backupByParent.set(parent, backup);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const forgetConsumerBackups = (): void => backupByParent.clear();
|
|
27
|
+
|
|
28
|
+
type SocketEntry = Record<string, { connected?: boolean } | undefined>;
|
|
29
|
+
|
|
30
|
+
/** The connected backup to prefer over `ip`, or null. */
|
|
31
|
+
export const preferredConsumerBackup = (ip: string, consumeSockets?: SocketEntry[] | null): string | null => {
|
|
32
|
+
const backup = backupByParent.get(ip);
|
|
33
|
+
if (!backup) return null;
|
|
34
|
+
const connected = (consumeSockets || []).some((entry) => {
|
|
35
|
+
const socket = entry?.[backup];
|
|
36
|
+
return Boolean(socket) && socket!.connected !== false;
|
|
37
|
+
});
|
|
38
|
+
return connected ? backup : null;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** Delay (ms) before consuming a stream announced by `ip`: non-zero only when a connected backup should win. */
|
|
42
|
+
export const consumerBackupPreferDelay = (ip: string, consumeSockets?: SocketEntry[] | null): number =>
|
|
43
|
+
preferredConsumerBackup(ip, consumeSockets) ? CONSUMER_BACKUP_PREFER_DELAY_MS : 0;
|