mediasfu-shared 1.0.2 → 1.0.4

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/package.json CHANGED
@@ -1,85 +1,99 @@
1
- {
2
- "name": "mediasfu-shared",
3
- "version": "1.0.2",
4
- "description": "Framework-agnostic MediaSFU runtime for room creation/join, mediasoup signaling, and shared browser state",
5
- "main": "dist/index.cjs",
6
- "module": "dist/index.js",
7
- "types": "dist/index.d.ts",
8
- "type": "module",
9
- "exports": {
10
- ".": {
11
- "types": "./dist/index.d.ts",
12
- "import": "./dist/index.js",
13
- "require": "./dist/index.cjs"
14
- },
15
- "./consumers": {
16
- "types": "./dist/consumers/index.d.ts",
17
- "import": "./dist/consumers/index.js",
18
- "require": "./dist/consumers/index.cjs"
19
- },
20
- "./methods": {
21
- "types": "./dist/methods/index.d.ts",
22
- "import": "./dist/methods/index.js",
23
- "require": "./dist/methods/index.cjs"
24
- },
25
- "./types": {
26
- "types": "./dist/types/index.d.ts",
27
- "import": "./dist/types/index.js",
28
- "require": "./dist/types/index.cjs"
29
- }
30
- },
31
- "files": [
32
- "dist",
33
- "src",
34
- "README.md",
35
- "LICENSE"
36
- ],
37
- "scripts": {
38
- "build": "tsc && vite build",
39
- "build-docs": "typedoc",
40
- "dev": "vite build --watch",
41
- "test:staging:smoke": "npm run build && node ./scripts/staging-room-smoke-test.cjs",
42
- "type-check": "tsc --noEmit"
43
- },
44
- "keywords": [
45
- "mediasfu",
46
- "webrtc",
47
- "mediasoup",
48
- "socket.io",
49
- "video-conferencing",
50
- "real-time-communication",
51
- "room-management",
52
- "screen-sharing",
53
- "recording",
54
- "translation",
55
- "framework-agnostic",
56
- "typescript"
57
- ],
58
- "author": "MediaSFU",
59
- "license": "MIT",
60
- "repository": {
61
- "type": "git",
62
- "url": "https://github.com/MediaSFU/MediaSFU-Shared"
63
- },
64
- "homepage": "https://github.com/MediaSFU/MediaSFU-Shared",
65
- "bugs": {
66
- "url": "https://github.com/MediaSFU/MediaSFU-Shared/issues",
67
- "email": "info@mediasfu.com"
68
- },
69
- "peerDependencies": {
70
- "mediasoup-client": "^3.16.0",
71
- "socket.io-client": "^4.8.0"
72
- },
73
- "dependencies": {
74
- "universal-cookie": "^7.2.2"
75
- },
76
- "devDependencies": {
77
- "mediasoup-client": "^3.16.0",
78
- "socket.io-client": "^4.8.0",
79
- "typedoc": "^0.28.14",
80
- "typedoc-plugin-extras": "^4.0.1",
81
- "typescript": "^5.9.3",
82
- "vite": "^7.1.9",
83
- "vite-plugin-dts": "^4.3.0"
84
- }
85
- }
1
+ {
2
+ "name": "mediasfu-shared",
3
+ "version": "1.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
+ "main": "dist/index.cjs",
6
+ "module": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "type": "module",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ },
15
+ "./consumers": {
16
+ "types": "./dist/consumers/index.d.ts",
17
+ "import": "./dist/consumers/index.js",
18
+ "require": "./dist/consumers/index.cjs"
19
+ },
20
+ "./methods": {
21
+ "types": "./dist/methods/index.d.ts",
22
+ "import": "./dist/methods/index.js",
23
+ "require": "./dist/methods/index.cjs"
24
+ },
25
+ "./types": {
26
+ "types": "./dist/types/index.d.ts",
27
+ "import": "./dist/types/index.js",
28
+ "require": "./dist/types/index.cjs"
29
+ }
30
+ },
31
+ "files": [
32
+ "dist",
33
+ "src",
34
+ "README.md",
35
+ "LICENSE"
36
+ ],
37
+ "scripts": {
38
+ "build": "tsc && vite build",
39
+ "build-docs": "typedoc",
40
+ "dev": "vite build --watch",
41
+ "test:staging:smoke": "npm run build && node ./scripts/staging-room-smoke-test.cjs",
42
+ "type-check": "tsc --noEmit"
43
+ },
44
+ "keywords": [
45
+ "mediasfu",
46
+ "mediasfu-shared",
47
+ "webrtc",
48
+ "mediasoup",
49
+ "socket.io",
50
+ "sfu",
51
+ "video conferencing",
52
+ "video call",
53
+ "webinar",
54
+ "live streaming",
55
+ "recording",
56
+ "whiteboard",
57
+ "translation",
58
+ "live subtitles",
59
+ "room management",
60
+ "screen sharing",
61
+ "real-time communication",
62
+ "framework-agnostic",
63
+ "typescript",
64
+ "react",
65
+ "vue",
66
+ "angular",
67
+ "svelte",
68
+ "mediasfu sdk",
69
+ "mediasfu runtime",
70
+ "mediasfu shared"
71
+ ],
72
+ "author": "MediaSFU",
73
+ "license": "MIT",
74
+ "repository": {
75
+ "type": "git",
76
+ "url": "https://github.com/MediaSFU/MediaSFU-Shared"
77
+ },
78
+ "homepage": "https://www.npmjs.com/package/mediasfu-shared",
79
+ "bugs": {
80
+ "url": "https://github.com/MediaSFU/MediaSFU-Shared/issues",
81
+ "email": "info@mediasfu.com"
82
+ },
83
+ "peerDependencies": {
84
+ "mediasoup-client": "^3.20.0",
85
+ "socket.io-client": "^4.8.0"
86
+ },
87
+ "dependencies": {
88
+ "universal-cookie": "^7.2.2"
89
+ },
90
+ "devDependencies": {
91
+ "mediasoup-client": "^3.20.0",
92
+ "socket.io-client": "^4.8.0",
93
+ "typedoc": "^0.28.14",
94
+ "typedoc-plugin-extras": "^4.0.1",
95
+ "typescript": "^5.9.3",
96
+ "vite": "^7.1.9",
97
+ "vite-plugin-dts": "^4.3.0"
98
+ }
99
+ }
@@ -125,13 +125,15 @@ export const connectIps = async ({
125
125
  }
126
126
 
127
127
  // Handle new pipe producer event
128
- remote_sock.on("new-pipe-producer", async ({ producerId, islevel }: { producerId: string; islevel: string }) => {
128
+ remote_sock.on("new-pipe-producer", async ({ producerId, islevel, isTranslation, translationMeta }: { producerId: string; islevel: string; isTranslation?: boolean; translationMeta?: { speakerId: string; speakerName: string; language: string; originalProducerId?: string; isSpeakerControlled?: boolean } }) => {
129
129
  if (newProducerMethod) {
130
130
  await newProducerMethod({
131
131
  producerId,
132
132
  islevel,
133
133
  nsock: remote_sock,
134
134
  parameters,
135
+ isTranslation,
136
+ translationMeta,
135
137
  });
136
138
  }
137
139
  });
@@ -76,13 +76,15 @@ export const connectLocalIps = async ({
76
76
 
77
77
  // Connect to the remote socket using socket.io-client
78
78
  // Handle new pipe producer event
79
- socket.on("new-producer", async ({ producerId, islevel }: { producerId: string; islevel: string }) => {
79
+ socket.on("new-producer", async ({ producerId, islevel, isTranslation, translationMeta }: { producerId: string; islevel: string; isTranslation?: boolean; translationMeta?: { speakerId: string; speakerName: string; language: string; originalProducerId?: string } }) => {
80
80
  if (newProducerMethod) {
81
81
  await newProducerMethod({
82
82
  producerId,
83
83
  islevel,
84
84
  nsock: socket,
85
85
  parameters,
86
+ isTranslation,
87
+ translationMeta,
86
88
  });
87
89
  }
88
90
  });
@@ -11,6 +11,74 @@ interface SpeakerTranslationState {
11
11
  enabled: boolean;
12
12
  }
13
13
 
14
+ const getSpeakerNameForProducerId = (
15
+ producerId: string,
16
+ parameters: Record<string, any>,
17
+ ): string | undefined => {
18
+ const participants = parameters.participants as Participant[] | undefined;
19
+ const participant = participants?.find((candidate) => candidate.audioID === producerId);
20
+
21
+ if (participant?.name) {
22
+ return participant.name;
23
+ }
24
+
25
+ const audStreamName = (parameters.audStreamNames as Array<{ producerId?: string; name?: string }> | undefined)
26
+ ?.find((stream) => stream.producerId === producerId && typeof stream.name === 'string');
27
+ if (audStreamName?.name) {
28
+ return audStreamName.name;
29
+ }
30
+
31
+ return (parameters.allAudioStreams as Array<{ producerId?: string; name?: string }> | undefined)
32
+ ?.find((stream) => stream.producerId === producerId && typeof stream.name === 'string')
33
+ ?.name;
34
+ };
35
+
36
+ const isOriginalAudioSuppressedByTranslation = (
37
+ producerId: string,
38
+ parameters: Record<string, any>,
39
+ ): boolean => {
40
+ const activeTranslationProducerIds = parameters.activeTranslationProducerIds as Set<string> | undefined;
41
+ if (activeTranslationProducerIds?.has(producerId)) {
42
+ return false;
43
+ }
44
+
45
+ const speakerTranslationStates = parameters.speakerTranslationStates as
46
+ | Map<string, SpeakerTranslationState>
47
+ | undefined;
48
+
49
+ if (!speakerTranslationStates?.size) {
50
+ return false;
51
+ }
52
+
53
+ const speakerName = getSpeakerNameForProducerId(producerId, parameters);
54
+
55
+ if (speakerName) {
56
+ const speakerState = speakerTranslationStates.get(speakerName);
57
+ if (speakerState?.enabled) {
58
+ return true;
59
+ }
60
+ }
61
+
62
+ return Array.from(speakerTranslationStates.values()).some((speakerState) => {
63
+ if (!speakerState?.enabled) {
64
+ return false;
65
+ }
66
+
67
+ if (speakerState.originalProducerId === producerId) {
68
+ return true;
69
+ }
70
+
71
+ if (!speakerName) {
72
+ return false;
73
+ }
74
+
75
+ return (
76
+ speakerState.speakerId === speakerName ||
77
+ speakerState.speakerName === speakerName
78
+ );
79
+ });
80
+ };
81
+
14
82
  interface Params {
15
83
  id: string;
16
84
  producerId: string;
@@ -152,28 +220,14 @@ export const connectRecvTransport = async ({
152
220
  if (params.kind === 'audio') {
153
221
  try {
154
222
  const updatedParams = parameters.getUpdatedAllParams();
155
- const speakerTranslationStates = updatedParams.speakerTranslationStates as
156
- | Map<string, SpeakerTranslationState>
157
- | undefined;
158
- const participants = updatedParams.participants as Participant[] | undefined;
159
-
160
- if (speakerTranslationStates && speakerTranslationStates.size > 0 && participants?.length) {
161
- const participant = participants.find(
162
- (candidate) => candidate.audioID === remoteProducerId,
223
+ if (isOriginalAudioSuppressedByTranslation(remoteProducerId, updatedParams)) {
224
+ consumer.pause();
225
+ nsock.emit(
226
+ 'consumer-pause',
227
+ { serverConsumerId: params.serverConsumerId },
228
+ () => {},
163
229
  );
164
-
165
- if (participant?.name) {
166
- const speakerState = speakerTranslationStates.get(participant.name);
167
-
168
- if (speakerState?.enabled && speakerState.originalProducerId === remoteProducerId) {
169
- consumer.pause();
170
- nsock.emit(
171
- 'consumer-pause',
172
- { serverConsumerId: params.serverConsumerId },
173
- () => {},
174
- );
175
- }
176
- }
230
+ return;
177
231
  }
178
232
  } catch {
179
233
  }
@@ -4,6 +4,18 @@ interface ProducerIdCarrier {
4
4
  producerId?: string | null;
5
5
  }
6
6
 
7
+ interface SpeakerTranslationStateLike {
8
+ enabled?: boolean;
9
+ speakerId?: string;
10
+ speakerName?: string;
11
+ originalProducerId?: string;
12
+ }
13
+
14
+ interface ParticipantLike {
15
+ name?: string | null;
16
+ audioID?: string | null;
17
+ }
18
+
7
19
  const getProducerId = (value: unknown): string | null | undefined => {
8
20
  return (value as ProducerIdCarrier | null | undefined)?.producerId;
9
21
  };
@@ -30,6 +42,78 @@ interface TransportLike {
30
42
  serverConsumerTransportId: string;
31
43
  }
32
44
 
45
+ const getSpeakerNameForProducerId = (
46
+ producerId: string,
47
+ parameters: Record<string, any>,
48
+ ): string | undefined => {
49
+ const participants = parameters.participants as ParticipantLike[] | undefined;
50
+ const participant = participants?.find((candidate) => candidate.audioID === producerId);
51
+
52
+ if (participant?.name) {
53
+ return participant.name;
54
+ }
55
+
56
+ const audStreamName = (parameters.audStreamNames as Array<{ producerId?: string; name?: string }> | undefined)
57
+ ?.find((stream) => stream.producerId === producerId && typeof stream.name === 'string');
58
+ if (audStreamName?.name) {
59
+ return audStreamName.name;
60
+ }
61
+
62
+ return (parameters.allAudioStreams as Array<{ producerId?: string; name?: string }> | undefined)
63
+ ?.find((stream) => stream.producerId === producerId && typeof stream.name === 'string')
64
+ ?.name;
65
+ };
66
+
67
+ const isOriginalAudioSuppressedByTranslation = (
68
+ producerId: string | null | undefined,
69
+ parameters: Record<string, any>,
70
+ ): boolean => {
71
+ if (!producerId) {
72
+ return false;
73
+ }
74
+
75
+ const activeTranslationProducerIds = parameters.activeTranslationProducerIds as Set<string> | undefined;
76
+ if (activeTranslationProducerIds?.has(producerId)) {
77
+ return false;
78
+ }
79
+
80
+ const speakerTranslationStates = parameters.speakerTranslationStates as
81
+ | Map<string, SpeakerTranslationStateLike>
82
+ | undefined;
83
+
84
+ if (!speakerTranslationStates?.size) {
85
+ return false;
86
+ }
87
+
88
+ const speakerName = getSpeakerNameForProducerId(producerId, parameters);
89
+
90
+ if (speakerName) {
91
+ const speakerState = speakerTranslationStates.get(speakerName);
92
+ if (speakerState?.enabled) {
93
+ return true;
94
+ }
95
+ }
96
+
97
+ return Array.from(speakerTranslationStates.values()).some((speakerState) => {
98
+ if (!speakerState?.enabled) {
99
+ return false;
100
+ }
101
+
102
+ if (speakerState.originalProducerId === producerId) {
103
+ return true;
104
+ }
105
+
106
+ if (!speakerName) {
107
+ return false;
108
+ }
109
+
110
+ return (
111
+ speakerState.speakerId === speakerName ||
112
+ speakerState.speakerName === speakerName
113
+ );
114
+ });
115
+ };
116
+
33
117
  export interface ProcessConsumerTransportsAudioParameters {
34
118
 
35
119
  // mediasfu functions
@@ -106,6 +190,7 @@ export const processConsumerTransportsAudio = async <
106
190
  const consumerTransportsToResume = consumerTransports.filter(
107
191
  (transport) =>
108
192
  isValidProducerId(transport.producerId, lStreams) &&
193
+ !isOriginalAudioSuppressedByTranslation(transport.producerId, parameters) &&
109
194
  transport.consumer?.paused === true &&
110
195
  transport.consumer?.kind === "audio"
111
196
  );
@@ -116,8 +201,11 @@ export const processConsumerTransportsAudio = async <
116
201
  transport.producerId &&
117
202
  transport.producerId !== null &&
118
203
  transport.producerId !== "" &&
119
- !lStreams.some(
120
- (stream) => getProducerId(stream) === transport.producerId
204
+ (
205
+ isOriginalAudioSuppressedByTranslation(transport.producerId, parameters) ||
206
+ !lStreams.some(
207
+ (stream) => getProducerId(stream) === transport.producerId
208
+ )
121
209
  ) &&
122
210
  transport.consumer &&
123
211
  transport.consumer?.kind &&
@@ -141,6 +229,10 @@ export const processConsumerTransportsAudio = async <
141
229
 
142
230
  // Emit consumer.resume() for each transport to resume
143
231
  for (const transport of consumerTransportsToResume) {
232
+ if (isOriginalAudioSuppressedByTranslation(transport.producerId, parameters)) {
233
+ continue;
234
+ }
235
+
144
236
  transport.socket_.emit(
145
237
  "consumer-resume",
146
238
  { serverConsumerId: transport.serverConsumerTransportId },
@@ -107,15 +107,25 @@ export const pauseOriginalProducer = async ({
107
107
  (t) => t.producerId === originalProducerId && t.consumer?.kind === 'audio'
108
108
  );
109
109
 
110
- if (transport && transport.consumer && !transport.consumer.paused) {
111
- transport.consumer.pause();
110
+ if (!transport?.consumer) {
111
+ return;
112
+ }
112
113
 
113
- transport.socket_?.emit(
114
- 'consumer-pause',
115
- { serverConsumerId: transport.serverConsumerTransportId },
116
- async () => {}
117
- );
114
+ if (transport.consumer.track) {
115
+ transport.consumer.track.enabled = false;
118
116
  }
117
+
118
+ if (transport.consumer.paused) {
119
+ return;
120
+ }
121
+
122
+ transport.consumer.pause();
123
+
124
+ transport.socket_?.emit(
125
+ 'consumer-pause',
126
+ { serverConsumerId: transport.serverConsumerTransportId },
127
+ async () => {}
128
+ );
119
129
  } catch (error) {
120
130
  console.error('[TranslationSwitch] Error pausing original producer:', error);
121
131
  }
@@ -137,17 +147,29 @@ export const resumeOriginalProducer = async ({
137
147
  (t) => t.producerId === originalProducerId && t.consumer?.kind === 'audio'
138
148
  );
139
149
 
140
- if (transport && transport.consumer && transport.consumer.paused) {
141
- transport.socket_?.emit(
142
- 'consumer-resume',
143
- { serverConsumerId: transport.serverConsumerTransportId },
144
- async ({ resumed }: { resumed: boolean }) => {
145
- if (resumed) {
146
- transport.consumer.resume();
150
+ if (!transport?.consumer) {
151
+ return;
152
+ }
153
+
154
+ if (!transport.consumer.paused) {
155
+ if (transport.consumer.track) {
156
+ transport.consumer.track.enabled = true;
157
+ }
158
+ return;
159
+ }
160
+
161
+ transport.socket_?.emit(
162
+ 'consumer-resume',
163
+ { serverConsumerId: transport.serverConsumerTransportId },
164
+ async ({ resumed }: { resumed: boolean }) => {
165
+ if (resumed) {
166
+ if (transport.consumer.track) {
167
+ transport.consumer.track.enabled = true;
147
168
  }
169
+ transport.consumer.resume();
148
170
  }
149
- );
150
- }
171
+ }
172
+ );
151
173
  } catch (error) {
152
174
  console.error('[TranslationSwitch] Error resuming original producer:', error);
153
175
  }
@@ -51,17 +51,22 @@ export const hostRequestResponse = async ({
51
51
  updateChatRequestTime,
52
52
  updateRequestIntervalSeconds,
53
53
  }: HostRequestResponseOptions): Promise<void> => {
54
+ const requestType = requestResponse.type ?? requestResponse.icon;
55
+
56
+ // Remove only the specific request that received a host response.
54
57
  const filteredRequests = requestList.filter(
55
- (request) =>
56
- request.id !== requestResponse.id &&
57
- request.icon !== requestResponse.type &&
58
- request.name !== requestResponse.name &&
59
- request.username !== requestResponse.username,
58
+ (request) => {
59
+ const matchesId = request.id === requestResponse.id;
60
+ const matchesType = requestType == null || request.icon === requestType;
61
+ const matchesName = requestResponse.name == null || request.name === requestResponse.name;
62
+ const matchesUsername =
63
+ requestResponse.username == null || request.username === requestResponse.username;
64
+
65
+ return !(matchesId && matchesType && matchesName && matchesUsername);
66
+ }
60
67
  );
61
68
  updateRequestList(filteredRequests);
62
69
 
63
- const requestType = requestResponse.type;
64
-
65
70
  if (requestResponse.action === 'accepted') {
66
71
  switch (requestType) {
67
72
  case 'fa-microphone':
@@ -164,7 +164,7 @@ export interface TranslationSubscribedOptions {
164
164
  data: TranslationSubscribedData;
165
165
  updateListenPreferences?: (updater: (prev: Map<string, string>) => Map<string, string>) => void;
166
166
  updateTranslationProducerMap?: (updater: (prev: TranslationProducerMap) => TranslationProducerMap) => void;
167
- startConsumingTranslation?: (producerId: string, speakerId: string, language: string) => Promise<void>;
167
+ startConsumingTranslation?: (producerId: string, speakerId: string, language: string, originalProducerId?: string) => Promise<void>;
168
168
  showAlert?: ShowAlert;
169
169
  }
170
170
 
@@ -323,7 +323,7 @@ export const translationSubscribed: TranslationSubscribedType = async ({
323
323
  }
324
324
 
325
325
  if (producerId && startConsumingTranslation) {
326
- await startConsumingTranslation(producerId, speakerId, language);
326
+ await startConsumingTranslation(producerId, speakerId, language, originalProducerId);
327
327
  }
328
328
 
329
329
  if (showAlert && channelCreated) {
@@ -208,7 +208,12 @@ export const clickVideo = async ({ parameters }: ClickVideoOptions): Promise<voi
208
208
  if (!videoAction && islevel !== "2" && !youAreCoHost) {
209
209
  response = await checkPermission({
210
210
  permissionType: "videoSetting",
211
- audioSetting, videoSetting, screenshareSetting, chatSetting,
211
+ audioSetting,
212
+ videoSetting,
213
+ screenshareSetting,
214
+ chatSetting,
215
+ permissionConfig: parameters.permissionConfig,
216
+ participantLevel: islevel,
212
217
  });
213
218
  } else {
214
219
  response = 0;
@@ -2,15 +2,27 @@ const DEFAULT_MEDIA_SFU_ROOM_API_URL = 'https://mediasfu.com/v1/rooms/';
2
2
 
3
3
  type MediaSFURoomApiAction = 'createRoom' | 'joinRoom';
4
4
 
5
+ const normalizeManagedRoomApi = (normalizedLink: string): string => {
6
+ if (normalizedLink.includes('/v1/rooms')) {
7
+ return `${normalizedLink.replace(/\/$/, '')}/`;
8
+ }
9
+
10
+ return `${normalizedLink.replace(/\/$/, '')}/v1/rooms/`;
11
+ };
12
+
5
13
  export const resolveMediaSFURoomApi = (
6
14
  localLink: string | undefined,
7
15
  action: MediaSFURoomApiAction,
8
16
  ): string => {
9
17
  const normalizedLink = localLink?.trim();
10
18
 
11
- if (!normalizedLink || normalizedLink.includes('mediasfu.com')) {
19
+ if (!normalizedLink) {
12
20
  return DEFAULT_MEDIA_SFU_ROOM_API_URL;
13
21
  }
14
22
 
23
+ if (normalizedLink.includes('mediasfu.com')) {
24
+ return normalizeManagedRoomApi(normalizedLink);
25
+ }
26
+
15
27
  return `${normalizedLink.replace(/\/$/, '')}/${action}`;
16
28
  };