@defcomm/meetingsdk 0.1.2 → 0.1.3

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/dist/index.d.mts CHANGED
@@ -285,6 +285,9 @@ declare class SFUClient {
285
285
  private videoSender;
286
286
  private audioSender;
287
287
  private screenSender;
288
+ private audioTransceiver;
289
+ private cameraTransceiver;
290
+ private screenTransceiver;
288
291
  private iceServers;
289
292
  private requestId;
290
293
  private publisherRevision;
@@ -313,6 +316,7 @@ declare class SFUClient {
313
316
  private trackMetadata;
314
317
  private participants;
315
318
  private remoteStreams;
319
+ private remoteScreenStreams;
316
320
  private trackToParticipant;
317
321
  private trackToSource;
318
322
  private localTrackIds;
package/dist/index.d.ts CHANGED
@@ -285,6 +285,9 @@ declare class SFUClient {
285
285
  private videoSender;
286
286
  private audioSender;
287
287
  private screenSender;
288
+ private audioTransceiver;
289
+ private cameraTransceiver;
290
+ private screenTransceiver;
288
291
  private iceServers;
289
292
  private requestId;
290
293
  private publisherRevision;
@@ -313,6 +316,7 @@ declare class SFUClient {
313
316
  private trackMetadata;
314
317
  private participants;
315
318
  private remoteStreams;
319
+ private remoteScreenStreams;
316
320
  private trackToParticipant;
317
321
  private trackToSource;
318
322
  private localTrackIds;
package/dist/index.js CHANGED
@@ -387,6 +387,9 @@ var SFUClient = class {
387
387
  this.videoSender = null;
388
388
  this.audioSender = null;
389
389
  this.screenSender = null;
390
+ this.audioTransceiver = null;
391
+ this.cameraTransceiver = null;
392
+ this.screenTransceiver = null;
390
393
  this.iceServers = [];
391
394
  this.requestId = 0;
392
395
  this.publisherRevision = 0;
@@ -418,6 +421,7 @@ var SFUClient = class {
418
421
  this.trackMetadata = /* @__PURE__ */ new Map();
419
422
  this.participants = /* @__PURE__ */ new Map();
420
423
  this.remoteStreams = /* @__PURE__ */ new Map();
424
+ this.remoteScreenStreams = /* @__PURE__ */ new Map();
421
425
  this.trackToParticipant = /* @__PURE__ */ new Map();
422
426
  this.trackToSource = /* @__PURE__ */ new Map();
423
427
  this.localTrackIds = /* @__PURE__ */ new Map();
@@ -900,16 +904,18 @@ var SFUClient = class {
900
904
  }
901
905
  case "screen_share_stopped": {
902
906
  const participantId = payload.participant_id;
903
- const stream = participantId ? this.remoteStreams.get(participantId) : null;
907
+ const screenStream = participantId ? this.remoteScreenStreams.get(participantId) : null;
904
908
  const trackId = payload.track_id;
905
- if (stream) {
906
- for (const track of stream.getVideoTracks()) {
907
- const source = this.trackToSource.get(this.normalizeRemoteTrackId(track.id));
908
- if (!trackId || this.normalizeRemoteTrackId(track.id) === String(trackId) || source === "screen") {
909
- stream.removeTrack(track);
909
+ if (screenStream) {
910
+ for (const track of screenStream.getVideoTracks()) {
911
+ if (!trackId || this.normalizeRemoteTrackId(track.id) === String(trackId)) {
912
+ screenStream.removeTrack(track);
910
913
  track.stop();
911
914
  }
912
915
  }
916
+ if (screenStream.getTracks().length === 0 && participantId) {
917
+ this.remoteScreenStreams.delete(participantId);
918
+ }
913
919
  }
914
920
  this.onScreenShareStopped(participantId, trackId);
915
921
  break;
@@ -978,6 +984,18 @@ var SFUClient = class {
978
984
  const config = { iceServers: this.iceServers };
979
985
  this.publisher = new RTCPeerConnection(config);
980
986
  this.subscriber = new RTCPeerConnection(config);
987
+ this.audioTransceiver = this.publisher.addTransceiver("audio", {
988
+ direction: "sendonly"
989
+ });
990
+ this.cameraTransceiver = this.publisher.addTransceiver("video", {
991
+ direction: "sendonly"
992
+ });
993
+ this.screenTransceiver = this.publisher.addTransceiver("video", {
994
+ direction: "inactive"
995
+ });
996
+ this.audioSender = this.audioTransceiver.sender;
997
+ this.videoSender = this.cameraTransceiver.sender;
998
+ this.screenSender = this.screenTransceiver.sender;
981
999
  this.publisher.onicecandidate = (event) => {
982
1000
  if (event.candidate) this.sendCandidate("publisher", event.candidate);
983
1001
  };
@@ -1017,25 +1035,61 @@ var SFUClient = class {
1017
1035
  return;
1018
1036
  }
1019
1037
  const participantId = metadata.participant_id;
1038
+ const source = String(
1039
+ metadata.source || this.trackToSource.get(metadata.track_id) || ""
1040
+ ).trim().toLowerCase();
1041
+ if (source === "screen" || source === "screen_share") {
1042
+ let screenStream = this.remoteScreenStreams.get(participantId);
1043
+ if (!screenStream) {
1044
+ screenStream = new MediaStream();
1045
+ this.remoteScreenStreams.set(participantId, screenStream);
1046
+ }
1047
+ for (const existing of screenStream.getVideoTracks()) {
1048
+ if (existing.id !== event.track.id) {
1049
+ screenStream.removeTrack(existing);
1050
+ existing.stop();
1051
+ }
1052
+ }
1053
+ if (!screenStream.getTracks().some((track) => track.id === event.track.id)) {
1054
+ screenStream.addTrack(event.track);
1055
+ }
1056
+ event.track.onended = () => {
1057
+ try {
1058
+ screenStream?.removeTrack(event.track);
1059
+ } catch {
1060
+ }
1061
+ if (screenStream?.getTracks().length === 0) {
1062
+ this.remoteScreenStreams.delete(participantId);
1063
+ this.onScreenShareStopped(
1064
+ participantId,
1065
+ metadata.track_id
1066
+ );
1067
+ }
1068
+ };
1069
+ this.onScreenShareStarted(participantId, metadata);
1070
+ this.onRemoteTrack(event.track, screenStream, {
1071
+ ...metadata,
1072
+ source: "screen"
1073
+ });
1074
+ return;
1075
+ }
1076
+ if (event.track.kind === "video" && source !== "camera") {
1077
+ this.onError(
1078
+ new Error(
1079
+ `Refusing to route unknown remote video source to camera: ${metadata.track_id}`
1080
+ )
1081
+ );
1082
+ return;
1083
+ }
1020
1084
  let remoteStream = this.remoteStreams.get(participantId);
1021
1085
  if (!remoteStream) {
1022
1086
  remoteStream = new MediaStream();
1023
1087
  this.remoteStreams.set(participantId, remoteStream);
1024
1088
  }
1025
- const source = metadata.source || this.trackToSource.get(metadata.track_id);
1026
- if (source === "screen") {
1027
- for (const existing of remoteStream.getVideoTracks()) {
1028
- if (existing.id !== event.track.id && this.trackToSource.get(existing.id) === "screen") {
1029
- remoteStream.removeTrack(existing);
1030
- existing.stop();
1031
- }
1032
- }
1033
- } else {
1034
- for (const existing of remoteStream.getTracks()) {
1035
- if (existing.kind === event.track.kind && existing.id !== event.track.id) {
1036
- remoteStream.removeTrack(existing);
1037
- existing.stop();
1038
- }
1089
+ for (const existing of remoteStream.getTracks()) {
1090
+ if (existing.kind === event.track.kind && existing.id !== event.track.id) {
1091
+ remoteStream.removeTrack(existing);
1092
+ existing.stop();
1039
1093
  }
1040
1094
  }
1041
1095
  if (!remoteStream.getTracks().some((track) => track.id === event.track.id)) {
@@ -1047,31 +1101,24 @@ var SFUClient = class {
1047
1101
  } catch {
1048
1102
  }
1049
1103
  };
1050
- if (source === "screen") {
1051
- this.onScreenShareStarted(participantId, metadata);
1052
- }
1053
- this.onRemoteTrack(event.track, remoteStream, metadata);
1104
+ this.onRemoteTrack(event.track, remoteStream, {
1105
+ ...metadata,
1106
+ source: event.track.kind === "audio" ? "audio" : "camera"
1107
+ });
1054
1108
  }
1055
1109
  normalizeRemoteTrackId(browserTrackId) {
1056
1110
  return browserTrackId?.startsWith("track-") ? browserTrackId.slice("track-".length) : browserTrackId;
1057
1111
  }
1058
1112
  resolveRemoteTrackMetadata(track, stream) {
1059
- const sfuTrackId = this.normalizeRemoteTrackId(track.id);
1060
- const exact = this.trackMetadata.get(sfuTrackId);
1061
- if (exact) return exact;
1062
- const byTrackId = this.trackMetadata.get(track.id);
1063
- if (byTrackId) return byTrackId;
1064
- const matching = [...this.trackMetadata.values()].find(
1065
- (entry) => entry.kind === track.kind
1066
- );
1067
- if (matching) return matching;
1068
- if (stream?.id) {
1069
- const participant = this.participants.get(stream.id);
1070
- if (participant) {
1071
- return [...this.trackMetadata.values()].find(
1072
- (entry) => entry.participant_id === participant.participant_id && entry.kind === track.kind
1073
- ) || null;
1074
- }
1113
+ const candidates = [
1114
+ this.normalizeRemoteTrackId(track.id),
1115
+ track.id,
1116
+ stream?.id ? this.normalizeRemoteTrackId(stream.id) : null,
1117
+ stream?.id || null
1118
+ ].filter(Boolean);
1119
+ for (const id of candidates) {
1120
+ const metadata = this.trackMetadata.get(id);
1121
+ if (metadata) return metadata;
1075
1122
  }
1076
1123
  return null;
1077
1124
  }
@@ -1202,20 +1249,19 @@ var SFUClient = class {
1202
1249
  }
1203
1250
  async publishExistingTracks(audio = true, video = true) {
1204
1251
  if (!this.publisher || !this.localStream) return;
1205
- this.audioSender = null;
1206
- this.videoSender = null;
1207
- for (const track of this.localStream.getTracks()) {
1208
- if (track.kind === "audio" && !audio) continue;
1209
- if (track.kind === "video" && !video) continue;
1210
- const sender = this.publisher.addTrack(track, this.localStream);
1211
- if (track.kind === "audio") this.audioSender = sender;
1212
- if (track.kind === "video") this.videoSender = sender;
1213
- }
1214
- if (this.screenStream?.getVideoTracks()[0]) {
1215
- const screenTrack = this.screenStream.getVideoTracks()[0];
1216
- this.screenSender = this.publisher.addTrack(screenTrack, this.screenStream);
1217
- }
1218
- if (!this.publisher.getSenders().length) return;
1252
+ if (!this.audioTransceiver || !this.cameraTransceiver || !this.screenTransceiver) {
1253
+ throw new Error("Publisher transceivers are not initialized");
1254
+ }
1255
+ const audioTrack = this.localStream.getAudioTracks().find((track) => track.readyState === "live");
1256
+ const cameraTrack = this.localStream.getVideoTracks().find((track) => track.readyState === "live");
1257
+ const screenTrack = this.screenStream?.getVideoTracks().find((track) => track.readyState === "live");
1258
+ await this.audioSender?.replaceTrack(audio && audioTrack ? audioTrack : null);
1259
+ this.audioTransceiver.direction = audio && audioTrack ? "sendonly" : "inactive";
1260
+ await this.videoSender?.replaceTrack(video && cameraTrack ? cameraTrack : null);
1261
+ this.cameraTransceiver.direction = video && cameraTrack ? "sendonly" : "inactive";
1262
+ await this.screenSender?.replaceTrack(screenTrack || null);
1263
+ this.screenTransceiver.direction = screenTrack ? "sendonly" : "inactive";
1264
+ if (!audioTrack && !cameraTrack && !screenTrack) return;
1219
1265
  const offer = await this.publisher.createOffer();
1220
1266
  await this.publisher.setLocalDescription(offer);
1221
1267
  this.send({
@@ -1225,44 +1271,86 @@ var SFUClient = class {
1225
1271
  sdp: offer.sdp
1226
1272
  }
1227
1273
  });
1228
- if (this.screenStream?.getVideoTracks()[0]) {
1274
+ if (screenTrack) {
1229
1275
  this.send({ type: "start_screen_share" });
1230
1276
  }
1231
1277
  }
1232
1278
  async shareScreen() {
1233
- if (!this.publisher) throw new Error("Publisher PeerConnection is not initialized");
1234
- if (this.screenStream) return this.screenStream;
1235
- this.screenStream = await navigator.mediaDevices.getDisplayMedia({ video: true });
1236
- const screenTrack = this.screenStream.getVideoTracks()[0];
1237
- if (!screenTrack) throw new Error("Screen share returned no video track");
1238
- this.screenSender = this.publisher.addTrack(screenTrack, this.screenStream);
1239
- const offer = await this.publisher.createOffer();
1240
- await this.publisher.setLocalDescription(offer);
1241
- this.send({
1242
- type: "publish_offer",
1243
- payload: {
1244
- revision: ++this.publisherRevision,
1245
- sdp: offer.sdp
1246
- }
1279
+ if (!this.publisher) {
1280
+ throw new Error("Publisher PeerConnection is not initialized");
1281
+ }
1282
+ if (!this.screenTransceiver || !this.screenSender) {
1283
+ throw new Error("Screen transceiver is not initialized");
1284
+ }
1285
+ if (this.screenStream) {
1286
+ return this.screenStream;
1287
+ }
1288
+ const screenStream = await navigator.mediaDevices.getDisplayMedia({
1289
+ video: true,
1290
+ audio: false
1247
1291
  });
1248
- this.send({ type: "start_screen_share" });
1249
- screenTrack.onended = () => {
1250
- void this.stopScreenShare();
1251
- };
1252
- return this.screenStream;
1253
- }
1254
- async stopScreenShare() {
1255
- if (!this.screenStream) return;
1256
- const screenTrack = this.screenStream.getVideoTracks()[0];
1257
- if (screenTrack) screenTrack.onended = null;
1258
- if (this.screenSender && this.publisher) {
1292
+ const screenTrack = screenStream.getVideoTracks()[0];
1293
+ if (!screenTrack) {
1294
+ screenStream.getTracks().forEach((track) => track.stop());
1295
+ throw new Error("Screen share returned no video track");
1296
+ }
1297
+ this.screenStream = screenStream;
1298
+ try {
1299
+ if (!this.send({ type: "start_screen_share" })) {
1300
+ throw new Error("Failed to signal start_screen_share");
1301
+ }
1302
+ await this.screenSender.replaceTrack(screenTrack);
1303
+ this.screenTransceiver.direction = "sendonly";
1304
+ const offer = await this.publisher.createOffer();
1305
+ await this.publisher.setLocalDescription(offer);
1306
+ this.send({
1307
+ type: "publish_offer",
1308
+ payload: {
1309
+ revision: ++this.publisherRevision,
1310
+ sdp: offer.sdp
1311
+ }
1312
+ });
1313
+ screenTrack.onended = () => {
1314
+ void this.stopScreenShare();
1315
+ };
1316
+ console.info("[publisher] Screen track attached", {
1317
+ screenTrackId: screenTrack.id,
1318
+ cameraTrackId: this.videoSender?.track?.id,
1319
+ screenMid: this.screenTransceiver.mid,
1320
+ cameraMid: this.cameraTransceiver?.mid
1321
+ });
1322
+ return this.screenStream;
1323
+ } catch (error) {
1259
1324
  try {
1260
- this.publisher.removeTrack(this.screenSender);
1325
+ await this.screenSender.replaceTrack(null);
1261
1326
  } catch {
1262
1327
  }
1263
- this.screenSender = null;
1328
+ this.screenTransceiver.direction = "inactive";
1329
+ screenStream.getTracks().forEach((track) => track.stop());
1330
+ this.screenStream = null;
1264
1331
  try {
1265
- const offer = await this.publisher.createOffer();
1332
+ this.send({ type: "stop_screen_share" });
1333
+ } catch {
1334
+ }
1335
+ throw error;
1336
+ }
1337
+ }
1338
+ async stopScreenShare() {
1339
+ if (!this.screenStream && !this.screenSender?.track) return;
1340
+ const screenTrack = this.screenSender?.track || null;
1341
+ if (screenTrack) screenTrack.onended = null;
1342
+ try {
1343
+ await this.screenSender?.replaceTrack(null);
1344
+ } catch (error) {
1345
+ this.onError(error);
1346
+ }
1347
+ if (this.screenTransceiver) {
1348
+ this.screenTransceiver.direction = "inactive";
1349
+ }
1350
+ this.send({ type: "stop_screen_share" });
1351
+ try {
1352
+ const offer = await this.publisher?.createOffer();
1353
+ if (offer && this.publisher) {
1266
1354
  await this.publisher.setLocalDescription(offer);
1267
1355
  this.send({
1268
1356
  type: "publish_offer",
@@ -1271,13 +1359,15 @@ var SFUClient = class {
1271
1359
  sdp: offer.sdp
1272
1360
  }
1273
1361
  });
1274
- this.send({ type: "stop_screen_share" });
1275
- } catch (error) {
1276
- this.onError(error);
1277
1362
  }
1363
+ } catch (error) {
1364
+ this.onError(error);
1278
1365
  }
1279
- this.screenStream.getTracks().forEach((track) => track.stop());
1366
+ this.screenStream?.getTracks().forEach((track) => track.stop());
1280
1367
  this.screenStream = null;
1368
+ if (this.currentParticipantId) {
1369
+ this.onScreenShareStopped(this.currentParticipantId);
1370
+ }
1281
1371
  }
1282
1372
  muteAudio(muted) {
1283
1373
  this.localStream?.getAudioTracks().forEach((track) => {
@@ -1363,6 +1453,11 @@ var SFUClient = class {
1363
1453
  stream.getTracks().forEach((track) => track.stop());
1364
1454
  this.remoteStreams.delete(participantId);
1365
1455
  }
1456
+ const screenStream = this.remoteScreenStreams.get(participantId);
1457
+ if (screenStream) {
1458
+ screenStream.getTracks().forEach((track) => track.stop());
1459
+ this.remoteScreenStreams.delete(participantId);
1460
+ }
1366
1461
  for (const [trackId, metadata] of this.trackMetadata) {
1367
1462
  if (metadata.participant_id === participantId) {
1368
1463
  this.trackMetadata.delete(trackId);
@@ -1438,6 +1533,9 @@ var SFUClient = class {
1438
1533
  this.videoSender = null;
1439
1534
  this.audioSender = null;
1440
1535
  this.screenSender = null;
1536
+ this.audioTransceiver = null;
1537
+ this.cameraTransceiver = null;
1538
+ this.screenTransceiver = null;
1441
1539
  this.pendingIce = { publisher: [], subscriber: [] };
1442
1540
  this.offerQueue = [];
1443
1541
  this.subscriberNegotiating = false;
@@ -1460,6 +1558,10 @@ var SFUClient = class {
1460
1558
  stream.getTracks().forEach((track) => track.stop());
1461
1559
  }
1462
1560
  this.remoteStreams.clear();
1561
+ for (const stream of this.remoteScreenStreams.values()) {
1562
+ stream.getTracks().forEach((track) => track.stop());
1563
+ }
1564
+ this.remoteScreenStreams.clear();
1463
1565
  this.trackMetadata.clear();
1464
1566
  this.trackToParticipant.clear();
1465
1567
  this.trackToSource.clear();