mediasfu-shared 1.2.6 → 1.3.0
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/HEADLESS_GUIDE.md +12 -0
- package/README.md +14 -2
- package/dist/ProducerClient/producerClientEmits/roomVideoPolicy.d.ts +20 -0
- package/dist/ProducerClient/producerClientEmits/roomVideoPolicy.d.ts.map +1 -0
- package/dist/ProducerClient/producerClientEmits/updateRoomParametersClient.d.ts +11 -1
- 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/connectSendTransportScreenAudio.d.ts +65 -0
- package/dist/consumers/connectSendTransportScreenAudio.d.ts.map +1 -0
- package/dist/consumers/consumerBackupPreference.d.ts +14 -0
- package/dist/consumers/consumerBackupPreference.d.ts.map +1 -0
- 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/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-B6LsMxmS.cjs → getParticipantMedia-1okyF4H7.cjs} +4 -4
- package/dist/{getParticipantMedia-B6LsMxmS.cjs.map → getParticipantMedia-1okyF4H7.cjs.map} +1 -1
- package/dist/{getParticipantMedia-B8MFkrT1.js → getParticipantMedia-DEJwXXhU.js} +4 -4
- package/dist/{getParticipantMedia-B8MFkrT1.js.map → getParticipantMedia-DEJwXXhU.js.map} +1 -1
- package/dist/index.cjs +36 -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 +1523 -1493
- package/dist/index.native.cjs +36 -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 +105 -75
- package/dist/{joinLocalRoom-X3NXzGmE.js → joinLocalRoom-DS6_6lJL.js} +26 -23
- package/dist/joinLocalRoom-DS6_6lJL.js.map +1 -0
- package/dist/{joinLocalRoom-XjGmL1_k.cjs → joinLocalRoom-Dsn39SwS.cjs} +26 -23
- package/dist/joinLocalRoom-Dsn39SwS.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 +12 -8
- package/dist/methods/index.cjs.map +1 -1
- package/dist/methods/index.js +14 -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/utils/createResponseJoinRoom.d.ts.map +1 -1
- package/dist/methods/welcome/handleWelcomeRequest.d.ts.map +1 -1
- 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 +8 -0
- package/dist/types/shared-base-types.d.ts.map +1 -1
- package/dist/{updateParticipantAudioDecibels-DiJ_LFfH.js → updateParticipantAudioDecibels-BBfdHWQ0.js} +344 -61
- package/dist/updateParticipantAudioDecibels-BBfdHWQ0.js.map +1 -0
- package/dist/{updateParticipantAudioDecibels-BJ2PxJJD.cjs → updateParticipantAudioDecibels-Dni8i6AY.cjs} +306 -23
- package/dist/updateParticipantAudioDecibels-Dni8i6AY.cjs.map +1 -0
- package/package.json +3 -2
- package/src/ProducerClient/producerClientEmits/roomVideoPolicy.ts +41 -0
- package/src/ProducerClient/producerClientEmits/updateRoomParametersClient.ts +19 -21
- package/src/consumers/closeAndResize.ts +14 -0
- package/src/consumers/connectIps.ts +81 -8
- package/src/consumers/connectSendTransportScreenAudio.ts +184 -0
- package/src/consumers/consumerBackupPreference.ts +43 -0
- 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/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/utils/createResponseJoinRoom.ts +2 -1
- package/src/methods/welcome/handleWelcomeRequest.ts +15 -6
- package/src/sockets/SocketManager.ts +76 -12
- package/src/sockets/mediaSocketErrors.ts +156 -0
- package/src/types/shared-base-types.ts +8 -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-X3NXzGmE.js.map +0 -1
- package/dist/joinLocalRoom-XjGmL1_k.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.0",
|
|
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",
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Camera-only policy. Screen-sharing and audio encodings are unchanged.
|
|
3
|
+
* Server capacity wins over a hint; recording does not increase camera bitrate.
|
|
4
|
+
*/
|
|
5
|
+
export interface CameraEncodingPolicy {
|
|
6
|
+
simulcastOff?: boolean;
|
|
7
|
+
roomCapacity?: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function cameraSimulcastOff(
|
|
11
|
+
eventType: string,
|
|
12
|
+
capacity: number | undefined,
|
|
13
|
+
options: CameraEncodingPolicy = {},
|
|
14
|
+
): boolean {
|
|
15
|
+
const roomCapacity = capacity ?? options.roomCapacity;
|
|
16
|
+
return options.simulcastOff === true || eventType === 'chat' || roomCapacity === 2;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type CameraEncoding = {
|
|
20
|
+
rid?: string;
|
|
21
|
+
scaleResolutionDownBy?: number;
|
|
22
|
+
scalabilityMode?: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** Mutate only the fresh room preset, preserving its original-preset lineage. */
|
|
26
|
+
export function applyCameraEncodingPolicy<T extends { encodings: CameraEncoding[] }>(
|
|
27
|
+
params: T,
|
|
28
|
+
singleEncoding: boolean,
|
|
29
|
+
): T {
|
|
30
|
+
if (!singleEncoding || !params.encodings.length) return params;
|
|
31
|
+
// Presets conventionally put the full-resolution layer last. Do not assume
|
|
32
|
+
// that order for custom presets; select the least downscaled encoding.
|
|
33
|
+
const full = params.encodings.reduce((best, encoding) =>
|
|
34
|
+
(encoding.scaleResolutionDownBy ?? 1) <= (best.scaleResolutionDownBy ?? 1)
|
|
35
|
+
? encoding : best);
|
|
36
|
+
const encoding = { ...full, scaleResolutionDownBy: 1 };
|
|
37
|
+
delete encoding.rid;
|
|
38
|
+
if (encoding.scalabilityMode) encoding.scalabilityMode = 'L1T3';
|
|
39
|
+
params.encodings = [encoding];
|
|
40
|
+
return params;
|
|
41
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { applyCameraEncodingPolicy, cameraSimulcastOff } from './roomVideoPolicy';
|
|
1
2
|
/* eslint-disable eqeqeq */
|
|
2
3
|
import { RtpCapabilities } from 'mediasoup-client/types';
|
|
3
4
|
import {
|
|
@@ -69,6 +70,10 @@ export interface UpdateRoomParametersClientParameters {
|
|
|
69
70
|
islevel: string;
|
|
70
71
|
showAlert?: ShowAlert;
|
|
71
72
|
data: ResponseJoinRoom;
|
|
73
|
+
/** Disable camera simulcast even in larger rooms. */
|
|
74
|
+
simulcastOff?: boolean;
|
|
75
|
+
/** Capacity hint used only when the server does not return capacity. */
|
|
76
|
+
roomCapacity?: number;
|
|
72
77
|
updateRtpCapabilities: (rtpCapabilities: RtpCapabilities | null) => void;
|
|
73
78
|
updateRoomRecvIPs: (roomRecvIPs: string[]) => void;
|
|
74
79
|
updateMeetingRoomParams: (meetingRoomParams: MeetingRoomParams | null) => void;
|
|
@@ -112,17 +117,23 @@ export interface UpdateRoomParametersClientParameters {
|
|
|
112
117
|
updateRecordingPreferredOrientation: (orientation: string) => void;
|
|
113
118
|
updateRecordingSupportForOtherOrientation: (support: boolean) => void;
|
|
114
119
|
updateRecordingMultiFormatsSupport: (support: boolean) => void;
|
|
120
|
+
/** Records whether the media server accepts screen-share audio (join response `supportsScreenAudio`). */
|
|
121
|
+
updateSupportsScreenAudio?: (supported: boolean) => void;
|
|
115
122
|
updateRecordingVideoOptions: (options: string) => void;
|
|
116
123
|
updateRecordingAudioOptions: (options: string) => void;
|
|
117
124
|
}
|
|
118
125
|
|
|
119
126
|
export type UpdateRoomParametersClientOptions = {
|
|
120
127
|
parameters: UpdateRoomParametersClientParameters;
|
|
128
|
+
/** Disable camera simulcast even in larger rooms. */
|
|
129
|
+
simulcastOff?: boolean;
|
|
130
|
+
/** Capacity hint used only when the server does not return capacity. */
|
|
131
|
+
roomCapacity?: number;
|
|
121
132
|
};
|
|
122
133
|
|
|
123
134
|
export type UpdateRoomParametersClientType = (options: UpdateRoomParametersClientOptions) => void;
|
|
124
135
|
|
|
125
|
-
export const updateRoomParametersClient = ({ parameters }: UpdateRoomParametersClientOptions): void => {
|
|
136
|
+
export const updateRoomParametersClient = ({ parameters, simulcastOff = parameters.simulcastOff, roomCapacity = parameters.roomCapacity }: UpdateRoomParametersClientOptions): void => {
|
|
126
137
|
try {
|
|
127
138
|
const {
|
|
128
139
|
screenPageLimit,
|
|
@@ -175,6 +186,7 @@ export const updateRoomParametersClient = ({ parameters }: UpdateRoomParametersC
|
|
|
175
186
|
updateRecordingPreferredOrientation,
|
|
176
187
|
updateRecordingSupportForOtherOrientation,
|
|
177
188
|
updateRecordingMultiFormatsSupport,
|
|
189
|
+
updateSupportsScreenAudio,
|
|
178
190
|
updateRecordingVideoOptions,
|
|
179
191
|
updateRecordingAudioOptions,
|
|
180
192
|
updateMainHeightWidth,
|
|
@@ -212,6 +224,7 @@ export const updateRoomParametersClient = ({ parameters }: UpdateRoomParametersC
|
|
|
212
224
|
updateRecordingPreferredOrientation(data.recordingParams.recordingPreferredOrientation);
|
|
213
225
|
updateRecordingSupportForOtherOrientation(data.recordingParams.recordingSupportForOtherOrientation);
|
|
214
226
|
updateRecordingMultiFormatsSupport(data.recordingParams.recordingMultiFormatsSupport);
|
|
227
|
+
updateSupportsScreenAudio?.(data.supportsScreenAudio === true);
|
|
215
228
|
|
|
216
229
|
updateItemPageLimit(data.meetingRoomParams.itemPageLimit);
|
|
217
230
|
updateEventType(data.meetingRoomParams.type);
|
|
@@ -354,26 +367,11 @@ export const updateRoomParametersClient = ({ parameters }: UpdateRoomParametersC
|
|
|
354
367
|
});
|
|
355
368
|
}
|
|
356
369
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
hParamsValue.encodings.forEach((encoding: Partial<HParamsType['encodings'][0]>) => {
|
|
365
|
-
if (encoding.maxBitrate) {
|
|
366
|
-
encoding.maxBitrate = Math.floor(encoding.maxBitrate * 1.2);
|
|
367
|
-
}
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
if (hParamsValue.codecOptions && hParamsValue.codecOptions.videoGoogleStartBitrate) {
|
|
371
|
-
hParamsValue.codecOptions.videoGoogleStartBitrate = Math.floor(hParamsValue.codecOptions.videoGoogleStartBitrate * 1.2);
|
|
372
|
-
}
|
|
373
|
-
if (vParamsValue.codecOptions && vParamsValue.codecOptions.videoGoogleStartBitrate) {
|
|
374
|
-
vParamsValue.codecOptions.videoGoogleStartBitrate = Math.floor(vParamsValue.codecOptions.videoGoogleStartBitrate * 1.2);
|
|
375
|
-
}
|
|
376
|
-
}
|
|
370
|
+
const singleCameraEncoding = cameraSimulcastOff(data.meetingRoomParams.type, data.capacity, {
|
|
371
|
+
simulcastOff, roomCapacity,
|
|
372
|
+
});
|
|
373
|
+
applyCameraEncodingPolicy(hParamsValue, singleCameraEncoding);
|
|
374
|
+
applyCameraEncodingPolicy(vParamsValue, singleCameraEncoding);
|
|
377
375
|
|
|
378
376
|
updateVidCons(vidCons);
|
|
379
377
|
updateFrameRate(frameRateValue);
|
|
@@ -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?.();
|
|
@@ -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
|
+
};
|
|
@@ -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;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Producer } from 'mediasoup-client/types';
|
|
2
2
|
import { Socket } from "socket.io-client";
|
|
3
|
+
import { disconnectSendTransportScreenAudio } from "./connectSendTransportScreenAudio";
|
|
3
4
|
|
|
4
5
|
export interface DisconnectSendTransportScreenParameters {
|
|
5
6
|
screenProducer: Producer | null;
|
|
@@ -94,6 +95,13 @@ export const disconnectSendTransportScreen = async ({ parameters }: DisconnectSe
|
|
|
94
95
|
let { getUpdatedAllParams } = parameters;
|
|
95
96
|
parameters = getUpdatedAllParams()
|
|
96
97
|
|
|
98
|
+
try {
|
|
99
|
+
// Screen audio stops with the screen share (the server closes its side on closeScreenProducer).
|
|
100
|
+
await disconnectSendTransportScreenAudio({ parameters: parameters as any });
|
|
101
|
+
} catch {
|
|
102
|
+
// nothing to close
|
|
103
|
+
}
|
|
104
|
+
|
|
97
105
|
try {
|
|
98
106
|
// Destructure parameters
|
|
99
107
|
let {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Socket } from "socket.io-client";
|
|
2
2
|
import { SignalNewConsumerTransportParameters, SignalNewConsumerTransportType } from '../types/types';
|
|
3
|
+
import { markScreenAudioProducer } from './screenAudio';
|
|
3
4
|
|
|
4
5
|
interface TranslationMeta {
|
|
5
6
|
speakerId: string;
|
|
@@ -12,6 +13,8 @@ interface TranslationMeta {
|
|
|
12
13
|
interface ProducerInfo {
|
|
13
14
|
id: string;
|
|
14
15
|
translationMeta?: TranslationMeta | null;
|
|
16
|
+
/** "screen-audio" for audio shared with a screen share. */
|
|
17
|
+
mediaTag?: string;
|
|
15
18
|
}
|
|
16
19
|
|
|
17
20
|
export interface GetPipedProducersAltParameters extends Omit<SignalNewConsumerTransportParameters, 'getUpdatedAllParams'> {
|
|
@@ -151,6 +154,7 @@ export const getPipedProducersAlt = async ({
|
|
|
151
154
|
} else {
|
|
152
155
|
producerId = producer.id;
|
|
153
156
|
translationMeta = producer.translationMeta || null;
|
|
157
|
+
markScreenAudioProducer(producerId, producer.mediaTag);
|
|
154
158
|
}
|
|
155
159
|
|
|
156
160
|
if (translationMeta) {
|
package/src/consumers/index.ts
CHANGED
|
@@ -13,6 +13,9 @@ export * from './connectRecvTransport';
|
|
|
13
13
|
export * from './connectSendTransport';
|
|
14
14
|
export * from './connectSendTransportAudio';
|
|
15
15
|
export * from './connectSendTransportScreen';
|
|
16
|
+
export * from './connectSendTransportScreenAudio';
|
|
17
|
+
export * from './screenAudio';
|
|
18
|
+
export * from './consumerBackupPreference';
|
|
16
19
|
export * from './connectSendTransportVideo';
|
|
17
20
|
export * from './consumerResume';
|
|
18
21
|
export * from './controlMedia';
|