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
|
@@ -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
|
}
|
|
@@ -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';
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type { Producer } from "mediasoup-client/types";
|
|
2
|
+
import { Participant } from "../types/types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Screen audio: the audio of a shared tab, window or screen, published as its own producer tagged
|
|
6
|
+
* `appData.mediaTag === "screen-audio"`. It belongs to the screen share (starts, pauses, resumes and stops with it),
|
|
7
|
+
* never to the sharer's microphone, so it never marks the sharer as speaking.
|
|
8
|
+
*
|
|
9
|
+
* A client only publishes it when the media server says so in the join response (`supportsScreenAudio`), so new
|
|
10
|
+
* clients keep today's video-only screen share on older servers.
|
|
11
|
+
*/
|
|
12
|
+
export const SCREEN_AUDIO_MEDIA_TAG = "screen-audio";
|
|
13
|
+
|
|
14
|
+
/** Opus settings for shared media (music, video soundtracks) rather than voice. */
|
|
15
|
+
export const SCREEN_AUDIO_CODEC_OPTIONS = {
|
|
16
|
+
opusStereo: true,
|
|
17
|
+
opusDtx: false,
|
|
18
|
+
opusFec: true,
|
|
19
|
+
opusMaxAverageBitrate: 128000,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// Producer ids are globally unique, so one registry serves every room instance on the page.
|
|
23
|
+
const screenAudioProducerIds = new Set<string>();
|
|
24
|
+
|
|
25
|
+
/** Records a producer announced by the server as screen audio (`mediaTag: "screen-audio"`). */
|
|
26
|
+
export const markScreenAudioProducer = (producerId?: string | null, mediaTag?: string | null): void => {
|
|
27
|
+
if (producerId && mediaTag === SCREEN_AUDIO_MEDIA_TAG) screenAudioProducerIds.add(producerId);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const forgetScreenAudioProducer = (producerId?: string | null): void => {
|
|
31
|
+
if (producerId) screenAudioProducerIds.delete(producerId);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export interface IsScreenAudioProducerOptions {
|
|
35
|
+
producerId: string;
|
|
36
|
+
participants?: Participant[];
|
|
37
|
+
appData?: Record<string, unknown> | null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** True when the producer is screen audio, by server tag, consumer appData or the sharer's `ScreenAudioID`. */
|
|
41
|
+
export const isScreenAudioProducer = ({ producerId, participants = [], appData }: IsScreenAudioProducerOptions): boolean => {
|
|
42
|
+
if (!producerId) return false;
|
|
43
|
+
if (screenAudioProducerIds.has(producerId)) return true;
|
|
44
|
+
if (appData?.mediaTag === SCREEN_AUDIO_MEDIA_TAG) return true;
|
|
45
|
+
return participants.some((participant) => participant?.ScreenAudioID === producerId);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* How a screen share treats audio: `ask` (default) shows a choice before the browser's picker, `always` requests audio
|
|
50
|
+
* without asking, `never` shares video only.
|
|
51
|
+
*/
|
|
52
|
+
export type ScreenAudioMode = "ask" | "always" | "never";
|
|
53
|
+
|
|
54
|
+
export interface ScreenAudioSettings {
|
|
55
|
+
supportsScreenAudio?: boolean;
|
|
56
|
+
screenAudioEnabled?: boolean;
|
|
57
|
+
screenAudioMode?: ScreenAudioMode;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Screen audio is requested only when the server supports it and the app has not turned it off. */
|
|
61
|
+
export const shouldShareScreenAudio = ({ supportsScreenAudio, screenAudioEnabled, screenAudioMode }: ScreenAudioSettings): boolean =>
|
|
62
|
+
supportsScreenAudio === true && screenAudioEnabled !== false && screenAudioMode !== "never";
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Whether this browser can capture the audio of a shared tab or screen. Chromium desktop browsers (Chrome, Edge,
|
|
66
|
+
* Opera, Brave) can; Firefox, Safari and mobile browsers return video only.
|
|
67
|
+
*/
|
|
68
|
+
export const canCaptureDisplayAudio = (nav: Partial<Navigator> | undefined = typeof navigator !== "undefined" ? navigator : undefined): boolean => {
|
|
69
|
+
if (!nav) return false;
|
|
70
|
+
const ua = String(nav.userAgent || "");
|
|
71
|
+
if (/Mobi|Android|iPhone|iPad|iPod/i.test(ua)) return false;
|
|
72
|
+
const brands: string[] = ((nav as any).userAgentData?.brands || []).map((entry: { brand?: string }) => String(entry?.brand || ""));
|
|
73
|
+
if (brands.some((brand) => /Chromium|Google Chrome|Microsoft Edge|Opera|Brave/i.test(brand))) return true;
|
|
74
|
+
return /(Chrome|Chromium|Edg)\//.test(ua) && !/Firefox|FxiOS|OPiOS/.test(ua);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/** The user's answer to "share audio too?": `audio`, `video` (screen only) or `null` (cancelled). */
|
|
78
|
+
export type ScreenAudioChoice = "audio" | "video" | null;
|
|
79
|
+
|
|
80
|
+
/** Mutes or unmutes the sharer's screen audio without stopping the screen share. Returns the new paused state. */
|
|
81
|
+
export const setScreenAudioPaused = (producer: Producer | null | undefined, paused: boolean): boolean => {
|
|
82
|
+
if (!producer || producer.closed) return false;
|
|
83
|
+
if (paused && !producer.paused) producer.pause();
|
|
84
|
+
if (!paused && producer.paused) producer.resume();
|
|
85
|
+
return producer.paused;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export interface DisplayMediaOptionsInput {
|
|
89
|
+
targetWidth: number;
|
|
90
|
+
targetHeight: number;
|
|
91
|
+
withAudio: boolean;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Options for `getDisplayMedia`. With audio, the browser's picker shows its own "share audio" choice, which is the
|
|
96
|
+
* user's consent; browsers that cannot capture display audio (Firefox, Safari) return a video-only stream.
|
|
97
|
+
*/
|
|
98
|
+
export const buildDisplayMediaOptions = ({ targetWidth, targetHeight, withAudio }: DisplayMediaOptionsInput) => {
|
|
99
|
+
const options: Record<string, unknown> = {
|
|
100
|
+
video: { width: targetWidth, height: targetHeight, frameRate: 30 },
|
|
101
|
+
audio: withAudio
|
|
102
|
+
? { echoCancellation: false, noiseSuppression: false, autoGainControl: false }
|
|
103
|
+
: false,
|
|
104
|
+
};
|
|
105
|
+
if (withAudio) {
|
|
106
|
+
// Chromium hints; other browsers ignore unknown members.
|
|
107
|
+
options.systemAudio = "include";
|
|
108
|
+
options.suppressLocalAudioPlayback = false;
|
|
109
|
+
}
|
|
110
|
+
return options as DisplayMediaStreamOptions;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// Fallback holder for apps whose parameters have no `screenAudioProducer` state.
|
|
114
|
+
const producersBySocket = new WeakMap<object, Producer>();
|
|
115
|
+
|
|
116
|
+
export const rememberScreenAudioProducer = (socket: object | null | undefined, producer: Producer | null): void => {
|
|
117
|
+
if (!socket) return;
|
|
118
|
+
if (producer) producersBySocket.set(socket, producer);
|
|
119
|
+
else producersBySocket.delete(socket);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export const recallScreenAudioProducer = (socket: object | null | undefined): Producer | null =>
|
|
123
|
+
(socket && producersBySocket.get(socket)) || null;
|
|
124
|
+
|
|
125
|
+
/** Sets the listening volume (0..1) of every screen-audio player on the page. */
|
|
126
|
+
export const setScreenAudioVolume = (volume: number, root: ParentNode | null = typeof document !== "undefined" ? document : null): void => {
|
|
127
|
+
if (!root) return;
|
|
128
|
+
const value = Math.min(1, Math.max(0, Number.isFinite(volume) ? volume : 1));
|
|
129
|
+
root.querySelectorAll<HTMLAudioElement>('audio[data-screen-audio="true"]').forEach((element) => {
|
|
130
|
+
element.volume = value;
|
|
131
|
+
});
|
|
132
|
+
};
|
|
@@ -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
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Transport, CloseAndResizeParameters, CloseAndResizeType } from '../../types/types';
|
|
2
|
+
import { forgetScreenAudioProducer } from '../screenAudio';
|
|
2
3
|
|
|
3
4
|
export interface ProducerClosedParameters extends CloseAndResizeParameters {
|
|
4
5
|
consumerTransports: Transport[];
|
|
@@ -63,6 +64,8 @@ export const producerClosed = async ({
|
|
|
63
64
|
}: ProducerClosedOptions): Promise<void> => {
|
|
64
65
|
let { consumerTransports, closeAndResize, screenId, updateConsumerTransports } = parameters;
|
|
65
66
|
|
|
67
|
+
forgetScreenAudioProducer(remoteProducerId);
|
|
68
|
+
|
|
66
69
|
const activeTranslationProducerIds = parameters.activeTranslationProducerIds;
|
|
67
70
|
const isTranslationProducer = activeTranslationProducerIds?.has?.(remoteProducerId);
|
|
68
71
|
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
|
|
2
2
|
import { StreamSuccessScreenType, StreamSuccessScreenParameters, ShowAlert } from "../types/types";
|
|
3
|
+
import {
|
|
4
|
+
buildDisplayMediaOptions,
|
|
5
|
+
canCaptureDisplayAudio,
|
|
6
|
+
shouldShareScreenAudio,
|
|
7
|
+
ScreenAudioChoice,
|
|
8
|
+
ScreenAudioMode,
|
|
9
|
+
} from "./screenAudio";
|
|
3
10
|
export interface StartShareScreenParameters extends StreamSuccessScreenParameters {
|
|
4
11
|
shared: boolean;
|
|
5
12
|
showAlert?: ShowAlert;
|
|
@@ -8,6 +15,19 @@ export interface StartShareScreenParameters extends StreamSuccessScreenParameter
|
|
|
8
15
|
onWeb: boolean;
|
|
9
16
|
targetWidth?: number;
|
|
10
17
|
targetHeight?: number;
|
|
18
|
+
/** Set from the join response: the media server accepts screen audio. */
|
|
19
|
+
supportsScreenAudio?: boolean;
|
|
20
|
+
/** Ask the browser for the screen's audio too (default true; the browser's picker still lets the user decide). */
|
|
21
|
+
screenAudioEnabled?: boolean;
|
|
22
|
+
/** `ask` (default): offer "Screen + audio" / "Screen only" first; `always`: request audio; `never`: video only. */
|
|
23
|
+
screenAudioMode?: ScreenAudioMode;
|
|
24
|
+
/**
|
|
25
|
+
* The "share audio too?" dialog: resolve `audio`, `video` or `null` (cancel). Each UI SDK supplies its own. Without
|
|
26
|
+
* one, `ask` requests audio and leaves the choice to the browser picker's own "share audio" checkbox.
|
|
27
|
+
*/
|
|
28
|
+
screenAudioPrompt?: () => Promise<ScreenAudioChoice>;
|
|
29
|
+
/** Override browser detection of display-audio capture (Chromium desktop by default). */
|
|
30
|
+
displayAudioCapable?: boolean;
|
|
11
31
|
|
|
12
32
|
// mediasfu functions
|
|
13
33
|
streamSuccessScreen: StreamSuccessScreenType;
|
|
@@ -71,6 +91,11 @@ export async function startShareScreen({ parameters }: StartShareScreenOptions):
|
|
|
71
91
|
onWeb,
|
|
72
92
|
targetWidth = 1280,
|
|
73
93
|
targetHeight = 720,
|
|
94
|
+
supportsScreenAudio,
|
|
95
|
+
screenAudioEnabled,
|
|
96
|
+
screenAudioMode = "ask",
|
|
97
|
+
screenAudioPrompt,
|
|
98
|
+
displayAudioCapable,
|
|
74
99
|
|
|
75
100
|
streamSuccessScreen,
|
|
76
101
|
} = parameters;
|
|
@@ -86,16 +111,27 @@ export async function startShareScreen({ parameters }: StartShareScreenOptions):
|
|
|
86
111
|
}
|
|
87
112
|
|
|
88
113
|
if (mediaDevices && mediaDevices.getDisplayMedia) {
|
|
114
|
+
// Screen audio needs a server that accepts it and a browser that can capture it (Chromium desktop).
|
|
115
|
+
let withAudio =
|
|
116
|
+
shouldShareScreenAudio({ supportsScreenAudio, screenAudioEnabled, screenAudioMode }) &&
|
|
117
|
+
(displayAudioCapable ?? canCaptureDisplayAudio());
|
|
118
|
+
if (withAudio && screenAudioMode === "ask" && screenAudioPrompt) {
|
|
119
|
+
const choice = await screenAudioPrompt();
|
|
120
|
+
if (choice === null) {
|
|
121
|
+
updateShared(false);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
withAudio = choice === "audio";
|
|
125
|
+
}
|
|
89
126
|
shared = true;
|
|
90
127
|
await mediaDevices
|
|
91
|
-
.getDisplayMedia(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
})
|
|
128
|
+
.getDisplayMedia(
|
|
129
|
+
buildDisplayMediaOptions({
|
|
130
|
+
targetWidth,
|
|
131
|
+
targetHeight,
|
|
132
|
+
withAudio,
|
|
133
|
+
})
|
|
134
|
+
)
|
|
99
135
|
.then(async (stream: MediaStream) => {
|
|
100
136
|
await streamSuccessScreen({ stream, parameters });
|
|
101
137
|
})
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
|
|
2
2
|
|
|
3
3
|
import { Socket } from "socket.io-client";
|
|
4
|
+
import { connectSendTransportScreenAudio } from "./connectSendTransportScreenAudio";
|
|
4
5
|
import {
|
|
5
6
|
SleepType, CreateSendTransportType, ConnectSendTransportScreenType, DisconnectSendTransportScreenType, StopShareScreenType, ReorderStreamsType, PrepopulateUserMediaType, RePortType,
|
|
6
7
|
ShowAlert, CreateSendTransportParameters, ConnectSendTransportScreenParameters, DisconnectSendTransportScreenParameters, StopShareScreenParameters, ReorderStreamsParameters, PrepopulateUserMediaParameters, RePortParameters,
|
|
@@ -179,6 +180,16 @@ export const streamSuccessScreen = async ({
|
|
|
179
180
|
|
|
180
181
|
// Alert the socket that you are sharing the screen
|
|
181
182
|
socket.emit("startScreenShare");
|
|
183
|
+
|
|
184
|
+
// Share the screen's audio too, when the browser captured it and the server supports it.
|
|
185
|
+
try {
|
|
186
|
+
await connectSendTransportScreenAudio({
|
|
187
|
+
stream: localStreamScreen,
|
|
188
|
+
parameters: getUpdatedAllParams() as any,
|
|
189
|
+
});
|
|
190
|
+
} catch (error) {
|
|
191
|
+
console.log("Screen audio not shared:", error);
|
|
192
|
+
}
|
|
182
193
|
} catch (error: any) {
|
|
183
194
|
showAlert?.({
|
|
184
195
|
message: error.message,
|
package/src/index.native.ts
CHANGED
|
@@ -17,6 +17,9 @@ export * from './consumers/connectSendTransport';
|
|
|
17
17
|
export * from './consumers/connectSendTransportAudio';
|
|
18
18
|
export * from './consumers/consumerResume';
|
|
19
19
|
export * from './consumers/connectSendTransportScreen';
|
|
20
|
+
export * from './consumers/connectSendTransportScreenAudio';
|
|
21
|
+
export * from './consumers/screenAudio';
|
|
22
|
+
export * from './consumers/consumerBackupPreference';
|
|
20
23
|
export * from './consumers/connectSendTransportVideo';
|
|
21
24
|
export * from './consumers/controlMedia';
|
|
22
25
|
export * from './consumers/createSendTransport';
|
|
@@ -217,6 +220,7 @@ export * from './producers/producerEmits/joinLocalRoom';
|
|
|
217
220
|
export * from './producers/producerEmits/joinRoom';
|
|
218
221
|
|
|
219
222
|
export * from './sockets/SocketManager';
|
|
223
|
+
export * from './sockets/mediaSocketErrors';
|
|
220
224
|
|
|
221
225
|
export * from './methods/stream/clickVideo';
|
|
222
226
|
|
package/src/index.ts
CHANGED
|
@@ -20,6 +20,9 @@ export * from './consumers/connectSendTransport';
|
|
|
20
20
|
export * from './consumers/connectSendTransportAudio';
|
|
21
21
|
export * from './consumers/consumerResume';
|
|
22
22
|
export * from './consumers/connectSendTransportScreen';
|
|
23
|
+
export * from './consumers/connectSendTransportScreenAudio';
|
|
24
|
+
export * from './consumers/screenAudio';
|
|
25
|
+
export * from './consumers/consumerBackupPreference';
|
|
23
26
|
export * from './consumers/connectSendTransportVideo';
|
|
24
27
|
export * from './consumers/controlMedia';
|
|
25
28
|
export * from './consumers/createSendTransport';
|
|
@@ -222,6 +225,7 @@ export * from './producers/producerEmits/joinLocalRoom';
|
|
|
222
225
|
export * from './producers/producerEmits/joinRoom';
|
|
223
226
|
// Socket exports
|
|
224
227
|
export * from './sockets/SocketManager';
|
|
228
|
+
export * from './sockets/mediaSocketErrors';
|
|
225
229
|
// Stream method exports (clickVideo)
|
|
226
230
|
export * from './methods/stream/clickVideo';
|
|
227
231
|
|
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
AltDomains,
|
|
4
4
|
ConsumeSocket,
|
|
5
5
|
} from '../../types/types';
|
|
6
|
+
import { rememberConsumerBackups } from "../../consumers/consumerBackupPreference";
|
|
6
7
|
|
|
7
8
|
export interface ConnectIpsLikeOptions<
|
|
8
9
|
TParameters = unknown,
|
|
@@ -88,6 +89,9 @@ export const getDomains = async <
|
|
|
88
89
|
let { roomRecvIPs, consume_sockets, connectIps } = parameters;
|
|
89
90
|
const ipsToConnect: string[] = [];
|
|
90
91
|
|
|
92
|
+
// Busy node -> backup map: new streams are preferred from the backup (consumer-backup-v1).
|
|
93
|
+
rememberConsumerBackups(alt_domains);
|
|
94
|
+
|
|
91
95
|
try {
|
|
92
96
|
consume_sockets = parameters.getUpdatedAllParams().consume_sockets;
|
|
93
97
|
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { PreJoinPageParameters } from "../../types/types";
|
|
2
|
+
import {
|
|
3
|
+
describeMediaSocketError,
|
|
4
|
+
isMediaSocketConnectError,
|
|
5
|
+
isNetworkOnlyMediaSocketError,
|
|
6
|
+
} from "../../sockets/mediaSocketErrors";
|
|
2
7
|
import type { Socket } from "socket.io-client";
|
|
3
8
|
import Cookies from "universal-cookie";
|
|
4
9
|
|
|
@@ -148,15 +153,19 @@ export const checkLimitsAndMakeRequestWithStorage: CheckLimitsAndMakeRequestWith
|
|
|
148
153
|
} catch (error) {
|
|
149
154
|
console.error('Error connecting to socket:', error);
|
|
150
155
|
parameters.showAlert?.({
|
|
151
|
-
message:
|
|
156
|
+
message: describeMediaSocketError(error),
|
|
152
157
|
type: 'danger',
|
|
153
|
-
duration: 3000,
|
|
158
|
+
duration: isMediaSocketConnectError(error) ? 6000 : 3000,
|
|
154
159
|
});
|
|
155
160
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
161
|
+
// Only a refusal or an unknown failure counts toward the attempt limit; a device/network problem is not a
|
|
162
|
+
// credential guess.
|
|
163
|
+
if (!isNetworkOnlyMediaSocketError(error)) {
|
|
164
|
+
let unsuccessfulAttempts = await readStoredNumber(storageAdapter, 'unsuccessfulAttempts');
|
|
165
|
+
unsuccessfulAttempts += 1;
|
|
166
|
+
await writeStoredNumber(storageAdapter, 'unsuccessfulAttempts', unsuccessfulAttempts);
|
|
167
|
+
await writeStoredNumber(storageAdapter, 'lastRequestTimestamp', Date.now());
|
|
168
|
+
}
|
|
160
169
|
parameters.updateIsLoadingModalVisible(false);
|
|
161
170
|
}
|
|
162
171
|
};
|
|
@@ -244,14 +253,18 @@ export const checkLimitsAndMakeRequest = async ({
|
|
|
244
253
|
duration: 3000,
|
|
245
254
|
});
|
|
246
255
|
}
|
|
247
|
-
} catch {
|
|
256
|
+
} catch (error) {
|
|
248
257
|
parameters.showAlert?.({
|
|
249
|
-
message:
|
|
258
|
+
message: describeMediaSocketError(error),
|
|
250
259
|
type: "danger",
|
|
251
|
-
duration: 3000,
|
|
260
|
+
duration: isMediaSocketConnectError(error) ? 6000 : 3000,
|
|
252
261
|
});
|
|
253
|
-
|
|
254
|
-
|
|
262
|
+
// Only a refusal or an unknown failure counts toward the attempt limit; a device/network problem is not a
|
|
263
|
+
// credential guess.
|
|
264
|
+
if (!isNetworkOnlyMediaSocketError(error)) {
|
|
265
|
+
unsuccessfulAttempts += 1;
|
|
266
|
+
cookies.set("unsuccessfulAttempts", unsuccessfulAttempts.toString());
|
|
267
|
+
}
|
|
255
268
|
parameters.updateIsLoadingModalVisible(false);
|
|
256
269
|
}
|
|
257
270
|
};
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import type { Socket } from 'socket.io-client';
|
|
2
|
+
import {
|
|
3
|
+
describeMediaSocketError,
|
|
4
|
+
isMediaSocketConnectError,
|
|
5
|
+
isNetworkOnlyMediaSocketError,
|
|
6
|
+
} from '../../sockets/mediaSocketErrors';
|
|
2
7
|
import Cookies from 'universal-cookie';
|
|
3
8
|
import type { PreJoinPageParameters } from '../../types/types';
|
|
4
9
|
|
|
@@ -183,16 +188,20 @@ export async function handleWelcomeRequest({
|
|
|
183
188
|
});
|
|
184
189
|
}
|
|
185
190
|
}
|
|
186
|
-
} catch {
|
|
191
|
+
} catch (error) {
|
|
187
192
|
showAlert?.({
|
|
188
|
-
message:
|
|
193
|
+
message: describeMediaSocketError(error),
|
|
189
194
|
type: 'danger',
|
|
190
|
-
duration: 3000,
|
|
195
|
+
duration: isMediaSocketConnectError(error) ? 6000 : 3000,
|
|
191
196
|
});
|
|
192
197
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
198
|
+
// Only a refusal or an unknown failure counts toward the attempt limit; a device/network problem is not a
|
|
199
|
+
// credential guess.
|
|
200
|
+
if (!isNetworkOnlyMediaSocketError(error)) {
|
|
201
|
+
unsuccessfulAttempts += 1;
|
|
202
|
+
cookies.set('unsuccessfulAttempts', unsuccessfulAttempts.toString());
|
|
203
|
+
cookies.set('lastRequestTimestamp', Date.now().toString());
|
|
204
|
+
}
|
|
196
205
|
updateIsLoadingModalVisible(false);
|
|
197
206
|
}
|
|
198
207
|
}
|