@edge-base/react-native 0.2.5 → 0.2.7
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/README.md +1 -52
- package/dist/index.cjs +370 -1560
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +76 -264
- package/dist/index.d.ts +76 -264
- package/dist/index.js +371 -1561
- package/dist/index.js.map +1 -1
- package/package.json +3 -11
package/dist/index.cjs
CHANGED
|
@@ -571,25 +571,23 @@ var AuthClient = class {
|
|
|
571
571
|
}
|
|
572
572
|
};
|
|
573
573
|
async function refreshAccessToken(baseUrl, refreshToken) {
|
|
574
|
+
const refreshUrl = `${baseUrl.replace(/\/$/, "")}/api/auth/refresh`;
|
|
574
575
|
let response;
|
|
575
576
|
try {
|
|
576
|
-
response = await fetch(
|
|
577
|
+
response = await fetch(refreshUrl, {
|
|
577
578
|
method: "POST",
|
|
578
579
|
headers: { "Content-Type": "application/json" },
|
|
579
580
|
body: JSON.stringify({ refreshToken })
|
|
580
581
|
});
|
|
581
582
|
} catch (error) {
|
|
582
|
-
throw
|
|
583
|
-
|
|
584
|
-
|
|
583
|
+
throw core.networkError(
|
|
584
|
+
`Auth session refresh could not reach ${refreshUrl}. Make sure the EdgeBase server is running and reachable.`,
|
|
585
|
+
{ cause: error }
|
|
585
586
|
);
|
|
586
587
|
}
|
|
587
588
|
const body = await response.json().catch(() => null);
|
|
588
589
|
if (!response.ok) {
|
|
589
|
-
throw
|
|
590
|
-
response.status,
|
|
591
|
-
typeof body?.message === "string" ? body.message : "Failed to refresh access token."
|
|
592
|
-
);
|
|
590
|
+
throw core.parseErrorResponse(response.status, body);
|
|
593
591
|
}
|
|
594
592
|
if (!body?.accessToken || !body?.refreshToken) {
|
|
595
593
|
throw new core.EdgeBaseError(500, "Invalid auth refresh response.");
|
|
@@ -758,7 +756,7 @@ var DatabaseLiveClient = class {
|
|
|
758
756
|
(refreshToken) => refreshAccessToken(this.baseUrl, refreshToken)
|
|
759
757
|
);
|
|
760
758
|
if (!token) throw new core.EdgeBaseError(401, "No access token available. Sign in first.");
|
|
761
|
-
this.sendRaw({ type: "auth", token, sdkVersion: "0.2.
|
|
759
|
+
this.sendRaw({ type: "auth", token, sdkVersion: "0.2.7" });
|
|
762
760
|
return new Promise((resolve, reject) => {
|
|
763
761
|
const timeout = setTimeout(() => reject(new core.EdgeBaseError(401, "Auth timeout")), 1e4);
|
|
764
762
|
const original = this.ws?.onmessage;
|
|
@@ -878,7 +876,7 @@ var DatabaseLiveClient = class {
|
|
|
878
876
|
refreshAuth() {
|
|
879
877
|
const token = this.tokenManager.currentAccessToken;
|
|
880
878
|
if (!token || !this.ws || !this.connected) return;
|
|
881
|
-
this.sendRaw({ type: "auth", token, sdkVersion: "0.2.
|
|
879
|
+
this.sendRaw({ type: "auth", token, sdkVersion: "0.2.7" });
|
|
882
880
|
}
|
|
883
881
|
handleAuthStateChange(user) {
|
|
884
882
|
if (user) {
|
|
@@ -1024,1110 +1022,6 @@ function matchesDatabaseLiveChannel(channel, change, messageChannel) {
|
|
|
1024
1022
|
}
|
|
1025
1023
|
return parts[3] === change.table && change.docId === parts[4];
|
|
1026
1024
|
}
|
|
1027
|
-
|
|
1028
|
-
// src/room-cloudflare-media.ts
|
|
1029
|
-
function buildRemoteTrackKey(participantId, kind) {
|
|
1030
|
-
return `${participantId}:${kind}`;
|
|
1031
|
-
}
|
|
1032
|
-
function installMessage(packageName) {
|
|
1033
|
-
return `Install ${packageName} to use React Native room media transport. See https://edgebase.fun/docs/room/media`;
|
|
1034
|
-
}
|
|
1035
|
-
var RoomCloudflareMediaTransport = class {
|
|
1036
|
-
room;
|
|
1037
|
-
options;
|
|
1038
|
-
localTracks = /* @__PURE__ */ new Map();
|
|
1039
|
-
remoteTrackHandlers = [];
|
|
1040
|
-
participantListeners = /* @__PURE__ */ new Map();
|
|
1041
|
-
remoteTrackIds = /* @__PURE__ */ new Map();
|
|
1042
|
-
clientFactoryPromise = null;
|
|
1043
|
-
connectPromise = null;
|
|
1044
|
-
lifecycleVersion = 0;
|
|
1045
|
-
meeting = null;
|
|
1046
|
-
sessionId = null;
|
|
1047
|
-
joinedMapSubscriptionsAttached = false;
|
|
1048
|
-
onParticipantJoined = (participant) => {
|
|
1049
|
-
this.attachParticipant(participant);
|
|
1050
|
-
};
|
|
1051
|
-
onParticipantLeft = (participant) => {
|
|
1052
|
-
this.detachParticipant(participant.id);
|
|
1053
|
-
};
|
|
1054
|
-
onParticipantsCleared = () => {
|
|
1055
|
-
this.clearParticipantListeners();
|
|
1056
|
-
};
|
|
1057
|
-
onParticipantsUpdate = () => {
|
|
1058
|
-
this.syncAllParticipants();
|
|
1059
|
-
};
|
|
1060
|
-
constructor(room, options) {
|
|
1061
|
-
this.room = room;
|
|
1062
|
-
this.options = {
|
|
1063
|
-
autoSubscribe: options?.autoSubscribe ?? true,
|
|
1064
|
-
mediaDevices: options?.mediaDevices ?? (typeof navigator !== "undefined" ? navigator.mediaDevices : void 0),
|
|
1065
|
-
clientFactory: options?.clientFactory
|
|
1066
|
-
};
|
|
1067
|
-
}
|
|
1068
|
-
getSessionId() {
|
|
1069
|
-
return this.sessionId;
|
|
1070
|
-
}
|
|
1071
|
-
getPeerConnection() {
|
|
1072
|
-
return null;
|
|
1073
|
-
}
|
|
1074
|
-
async connect(payload) {
|
|
1075
|
-
if (this.meeting && this.sessionId) {
|
|
1076
|
-
return this.sessionId;
|
|
1077
|
-
}
|
|
1078
|
-
if (this.connectPromise) {
|
|
1079
|
-
return this.connectPromise;
|
|
1080
|
-
}
|
|
1081
|
-
const connectPromise = (async () => {
|
|
1082
|
-
const lifecycleVersion = this.lifecycleVersion;
|
|
1083
|
-
const session = await this.room.media.cloudflareRealtimeKit.createSession(payload);
|
|
1084
|
-
this.assertConnectStillActive(lifecycleVersion);
|
|
1085
|
-
const factory = await this.resolveClientFactory();
|
|
1086
|
-
this.assertConnectStillActive(lifecycleVersion);
|
|
1087
|
-
const meeting = await factory.init({
|
|
1088
|
-
authToken: session.authToken,
|
|
1089
|
-
defaults: {
|
|
1090
|
-
audio: false,
|
|
1091
|
-
video: false
|
|
1092
|
-
}
|
|
1093
|
-
});
|
|
1094
|
-
this.assertConnectStillActive(lifecycleVersion, meeting);
|
|
1095
|
-
this.meeting = meeting;
|
|
1096
|
-
this.sessionId = session.sessionId;
|
|
1097
|
-
this.attachParticipantMapListeners();
|
|
1098
|
-
try {
|
|
1099
|
-
if (meeting.join) {
|
|
1100
|
-
await meeting.join();
|
|
1101
|
-
} else if (meeting.joinRoom) {
|
|
1102
|
-
await meeting.joinRoom();
|
|
1103
|
-
} else {
|
|
1104
|
-
throw new Error("RealtimeKit client does not expose join()/joinRoom().");
|
|
1105
|
-
}
|
|
1106
|
-
this.assertConnectStillActive(lifecycleVersion, meeting);
|
|
1107
|
-
this.syncAllParticipants();
|
|
1108
|
-
return session.sessionId;
|
|
1109
|
-
} catch (error) {
|
|
1110
|
-
if (this.meeting === meeting) {
|
|
1111
|
-
this.cleanupMeeting();
|
|
1112
|
-
} else {
|
|
1113
|
-
this.leaveMeetingSilently(meeting);
|
|
1114
|
-
}
|
|
1115
|
-
throw error;
|
|
1116
|
-
}
|
|
1117
|
-
})();
|
|
1118
|
-
this.connectPromise = connectPromise;
|
|
1119
|
-
try {
|
|
1120
|
-
return await connectPromise;
|
|
1121
|
-
} finally {
|
|
1122
|
-
if (this.connectPromise === connectPromise) {
|
|
1123
|
-
this.connectPromise = null;
|
|
1124
|
-
}
|
|
1125
|
-
}
|
|
1126
|
-
}
|
|
1127
|
-
async enableAudio(constraints = true) {
|
|
1128
|
-
const meeting = await this.ensureMeeting();
|
|
1129
|
-
const customTrack = await this.createUserMediaTrack("audio", constraints);
|
|
1130
|
-
await meeting.self.enableAudio(customTrack ?? void 0);
|
|
1131
|
-
const track = meeting.self.audioTrack ?? customTrack;
|
|
1132
|
-
if (!track) {
|
|
1133
|
-
throw new Error("RealtimeKit did not expose a local audio track after enabling audio.");
|
|
1134
|
-
}
|
|
1135
|
-
this.rememberLocalTrack("audio", track, track.getSettings().deviceId ?? customTrack?.getSettings().deviceId, !!customTrack);
|
|
1136
|
-
await this.room.media.audio.enable?.({
|
|
1137
|
-
trackId: track.id,
|
|
1138
|
-
deviceId: this.localTracks.get("audio")?.deviceId,
|
|
1139
|
-
providerSessionId: meeting.self.id
|
|
1140
|
-
});
|
|
1141
|
-
return track;
|
|
1142
|
-
}
|
|
1143
|
-
async enableVideo(constraints = true) {
|
|
1144
|
-
const meeting = await this.ensureMeeting();
|
|
1145
|
-
const customTrack = await this.createUserMediaTrack("video", constraints);
|
|
1146
|
-
await meeting.self.enableVideo(customTrack ?? void 0);
|
|
1147
|
-
const track = meeting.self.videoTrack ?? customTrack;
|
|
1148
|
-
if (!track) {
|
|
1149
|
-
throw new Error("RealtimeKit did not expose a local video track after enabling video.");
|
|
1150
|
-
}
|
|
1151
|
-
this.rememberLocalTrack("video", track, track.getSettings().deviceId ?? customTrack?.getSettings().deviceId, !!customTrack);
|
|
1152
|
-
await this.room.media.video.enable?.({
|
|
1153
|
-
trackId: track.id,
|
|
1154
|
-
deviceId: this.localTracks.get("video")?.deviceId,
|
|
1155
|
-
providerSessionId: meeting.self.id
|
|
1156
|
-
});
|
|
1157
|
-
return track;
|
|
1158
|
-
}
|
|
1159
|
-
async startScreenShare(_constraints = { video: true, audio: false }) {
|
|
1160
|
-
const meeting = await this.ensureMeeting();
|
|
1161
|
-
await meeting.self.enableScreenShare();
|
|
1162
|
-
const track = meeting.self.screenShareTracks?.video;
|
|
1163
|
-
if (!track) {
|
|
1164
|
-
throw new Error("RealtimeKit did not expose a screen-share video track.");
|
|
1165
|
-
}
|
|
1166
|
-
track.addEventListener("ended", () => {
|
|
1167
|
-
void this.stopScreenShare();
|
|
1168
|
-
}, { once: true });
|
|
1169
|
-
this.rememberLocalTrack("screen", track, track.getSettings().deviceId, false);
|
|
1170
|
-
await this.room.media.screen.start?.({
|
|
1171
|
-
trackId: track.id,
|
|
1172
|
-
deviceId: track.getSettings().deviceId,
|
|
1173
|
-
providerSessionId: meeting.self.id
|
|
1174
|
-
});
|
|
1175
|
-
return track;
|
|
1176
|
-
}
|
|
1177
|
-
async disableAudio() {
|
|
1178
|
-
if (!this.meeting) return;
|
|
1179
|
-
await this.meeting.self.disableAudio();
|
|
1180
|
-
this.releaseLocalTrack("audio");
|
|
1181
|
-
await this.room.media.audio.disable();
|
|
1182
|
-
}
|
|
1183
|
-
async disableVideo() {
|
|
1184
|
-
if (!this.meeting) return;
|
|
1185
|
-
await this.meeting.self.disableVideo();
|
|
1186
|
-
this.releaseLocalTrack("video");
|
|
1187
|
-
await this.room.media.video.disable();
|
|
1188
|
-
}
|
|
1189
|
-
async stopScreenShare() {
|
|
1190
|
-
if (!this.meeting) return;
|
|
1191
|
-
await this.meeting.self.disableScreenShare();
|
|
1192
|
-
this.releaseLocalTrack("screen");
|
|
1193
|
-
await this.room.media.screen.stop();
|
|
1194
|
-
}
|
|
1195
|
-
async setMuted(kind, muted) {
|
|
1196
|
-
const localTrack = this.localTracks.get(kind)?.track ?? (kind === "audio" ? this.meeting?.self.audioTrack : this.meeting?.self.videoTrack);
|
|
1197
|
-
if (localTrack) {
|
|
1198
|
-
localTrack.enabled = !muted;
|
|
1199
|
-
}
|
|
1200
|
-
if (kind === "audio") {
|
|
1201
|
-
await this.room.media.audio.setMuted?.(muted);
|
|
1202
|
-
} else {
|
|
1203
|
-
await this.room.media.video.setMuted?.(muted);
|
|
1204
|
-
}
|
|
1205
|
-
}
|
|
1206
|
-
async switchDevices(payload) {
|
|
1207
|
-
const meeting = await this.ensureMeeting();
|
|
1208
|
-
if (payload.audioInputId) {
|
|
1209
|
-
const audioDevice = await meeting.self.getDeviceById(payload.audioInputId, "audio");
|
|
1210
|
-
await meeting.self.setDevice(audioDevice);
|
|
1211
|
-
const audioTrack = meeting.self.audioTrack;
|
|
1212
|
-
if (audioTrack) {
|
|
1213
|
-
this.rememberLocalTrack("audio", audioTrack, payload.audioInputId, false);
|
|
1214
|
-
}
|
|
1215
|
-
}
|
|
1216
|
-
if (payload.videoInputId) {
|
|
1217
|
-
const videoDevice = await meeting.self.getDeviceById(payload.videoInputId, "video");
|
|
1218
|
-
await meeting.self.setDevice(videoDevice);
|
|
1219
|
-
const videoTrack = meeting.self.videoTrack;
|
|
1220
|
-
if (videoTrack) {
|
|
1221
|
-
this.rememberLocalTrack("video", videoTrack, payload.videoInputId, false);
|
|
1222
|
-
}
|
|
1223
|
-
}
|
|
1224
|
-
await this.room.media.devices.switch(payload);
|
|
1225
|
-
}
|
|
1226
|
-
onRemoteTrack(handler) {
|
|
1227
|
-
this.remoteTrackHandlers.push(handler);
|
|
1228
|
-
return core.createSubscription(() => {
|
|
1229
|
-
const index = this.remoteTrackHandlers.indexOf(handler);
|
|
1230
|
-
if (index >= 0) {
|
|
1231
|
-
this.remoteTrackHandlers.splice(index, 1);
|
|
1232
|
-
}
|
|
1233
|
-
});
|
|
1234
|
-
}
|
|
1235
|
-
destroy() {
|
|
1236
|
-
this.lifecycleVersion += 1;
|
|
1237
|
-
this.connectPromise = null;
|
|
1238
|
-
for (const kind of this.localTracks.keys()) {
|
|
1239
|
-
this.releaseLocalTrack(kind);
|
|
1240
|
-
}
|
|
1241
|
-
this.clearParticipantListeners();
|
|
1242
|
-
this.detachParticipantMapListeners();
|
|
1243
|
-
this.cleanupMeeting();
|
|
1244
|
-
}
|
|
1245
|
-
async ensureMeeting() {
|
|
1246
|
-
if (!this.meeting) {
|
|
1247
|
-
await this.connect();
|
|
1248
|
-
}
|
|
1249
|
-
if (!this.meeting) {
|
|
1250
|
-
throw new Error("Cloudflare media transport is not connected");
|
|
1251
|
-
}
|
|
1252
|
-
return this.meeting;
|
|
1253
|
-
}
|
|
1254
|
-
async resolveClientFactory() {
|
|
1255
|
-
if (this.options.clientFactory) {
|
|
1256
|
-
return this.options.clientFactory;
|
|
1257
|
-
}
|
|
1258
|
-
this.clientFactoryPromise ??= import('@cloudflare/realtimekit-react-native').then((mod) => mod.default ?? mod).catch((error) => {
|
|
1259
|
-
throw new Error(`${installMessage("@cloudflare/realtimekit-react-native")}
|
|
1260
|
-
${String(error)}`);
|
|
1261
|
-
});
|
|
1262
|
-
return this.clientFactoryPromise;
|
|
1263
|
-
}
|
|
1264
|
-
async createUserMediaTrack(kind, constraints) {
|
|
1265
|
-
const devices = this.options.mediaDevices;
|
|
1266
|
-
if (!devices?.getUserMedia || constraints === false) {
|
|
1267
|
-
return null;
|
|
1268
|
-
}
|
|
1269
|
-
const stream = await devices.getUserMedia(
|
|
1270
|
-
kind === "audio" ? { audio: constraints, video: false } : { audio: false, video: constraints }
|
|
1271
|
-
);
|
|
1272
|
-
return kind === "audio" ? stream.getAudioTracks()[0] ?? null : stream.getVideoTracks()[0] ?? null;
|
|
1273
|
-
}
|
|
1274
|
-
rememberLocalTrack(kind, track, deviceId, stopOnCleanup) {
|
|
1275
|
-
this.releaseLocalTrack(kind);
|
|
1276
|
-
this.localTracks.set(kind, {
|
|
1277
|
-
kind,
|
|
1278
|
-
track,
|
|
1279
|
-
deviceId,
|
|
1280
|
-
stopOnCleanup
|
|
1281
|
-
});
|
|
1282
|
-
}
|
|
1283
|
-
releaseLocalTrack(kind) {
|
|
1284
|
-
const local = this.localTracks.get(kind);
|
|
1285
|
-
if (!local) return;
|
|
1286
|
-
if (local.stopOnCleanup) {
|
|
1287
|
-
local.track.stop();
|
|
1288
|
-
}
|
|
1289
|
-
this.localTracks.delete(kind);
|
|
1290
|
-
}
|
|
1291
|
-
attachParticipantMapListeners() {
|
|
1292
|
-
const participantMap = this.getParticipantMap();
|
|
1293
|
-
if (!participantMap || !this.meeting || this.joinedMapSubscriptionsAttached) {
|
|
1294
|
-
return;
|
|
1295
|
-
}
|
|
1296
|
-
participantMap.on("participantJoined", this.onParticipantJoined);
|
|
1297
|
-
participantMap.on("participantLeft", this.onParticipantLeft);
|
|
1298
|
-
participantMap.on("participantsCleared", this.onParticipantsCleared);
|
|
1299
|
-
participantMap.on("participantsUpdate", this.onParticipantsUpdate);
|
|
1300
|
-
this.joinedMapSubscriptionsAttached = true;
|
|
1301
|
-
}
|
|
1302
|
-
detachParticipantMapListeners() {
|
|
1303
|
-
const participantMap = this.getParticipantMap();
|
|
1304
|
-
if (!participantMap || !this.meeting || !this.joinedMapSubscriptionsAttached) {
|
|
1305
|
-
return;
|
|
1306
|
-
}
|
|
1307
|
-
participantMap.off("participantJoined", this.onParticipantJoined);
|
|
1308
|
-
participantMap.off("participantLeft", this.onParticipantLeft);
|
|
1309
|
-
participantMap.off("participantsCleared", this.onParticipantsCleared);
|
|
1310
|
-
participantMap.off("participantsUpdate", this.onParticipantsUpdate);
|
|
1311
|
-
this.joinedMapSubscriptionsAttached = false;
|
|
1312
|
-
}
|
|
1313
|
-
syncAllParticipants() {
|
|
1314
|
-
const participantMap = this.getParticipantMap();
|
|
1315
|
-
if (!participantMap || !this.meeting || !this.options.autoSubscribe) {
|
|
1316
|
-
return;
|
|
1317
|
-
}
|
|
1318
|
-
for (const participant of participantMap.values()) {
|
|
1319
|
-
this.attachParticipant(participant);
|
|
1320
|
-
}
|
|
1321
|
-
}
|
|
1322
|
-
getParticipantMap() {
|
|
1323
|
-
if (!this.meeting) {
|
|
1324
|
-
return null;
|
|
1325
|
-
}
|
|
1326
|
-
return this.meeting.participants.active ?? this.meeting.participants.joined ?? null;
|
|
1327
|
-
}
|
|
1328
|
-
attachParticipant(participant) {
|
|
1329
|
-
if (!this.options.autoSubscribe || !this.meeting) {
|
|
1330
|
-
return;
|
|
1331
|
-
}
|
|
1332
|
-
if (participant.id === this.meeting.self.id || this.participantListeners.has(participant.id)) {
|
|
1333
|
-
this.syncParticipantTracks(participant);
|
|
1334
|
-
return;
|
|
1335
|
-
}
|
|
1336
|
-
const listenerSet = {
|
|
1337
|
-
participant,
|
|
1338
|
-
onAudioUpdate: ({ audioEnabled, audioTrack }) => {
|
|
1339
|
-
this.handleRemoteTrackUpdate("audio", participant, audioTrack, audioEnabled);
|
|
1340
|
-
},
|
|
1341
|
-
onVideoUpdate: ({ videoEnabled, videoTrack }) => {
|
|
1342
|
-
this.handleRemoteTrackUpdate("video", participant, videoTrack, videoEnabled);
|
|
1343
|
-
},
|
|
1344
|
-
onScreenShareUpdate: ({ screenShareEnabled, screenShareTracks }) => {
|
|
1345
|
-
this.handleRemoteTrackUpdate("screen", participant, screenShareTracks.video, screenShareEnabled);
|
|
1346
|
-
}
|
|
1347
|
-
};
|
|
1348
|
-
participant.on("audioUpdate", listenerSet.onAudioUpdate);
|
|
1349
|
-
participant.on("videoUpdate", listenerSet.onVideoUpdate);
|
|
1350
|
-
participant.on("screenShareUpdate", listenerSet.onScreenShareUpdate);
|
|
1351
|
-
this.participantListeners.set(participant.id, listenerSet);
|
|
1352
|
-
this.syncParticipantTracks(participant);
|
|
1353
|
-
}
|
|
1354
|
-
detachParticipant(participantId) {
|
|
1355
|
-
const listenerSet = this.participantListeners.get(participantId);
|
|
1356
|
-
if (!listenerSet) return;
|
|
1357
|
-
listenerSet.participant.off("audioUpdate", listenerSet.onAudioUpdate);
|
|
1358
|
-
listenerSet.participant.off("videoUpdate", listenerSet.onVideoUpdate);
|
|
1359
|
-
listenerSet.participant.off("screenShareUpdate", listenerSet.onScreenShareUpdate);
|
|
1360
|
-
this.participantListeners.delete(participantId);
|
|
1361
|
-
for (const kind of ["audio", "video", "screen"]) {
|
|
1362
|
-
this.remoteTrackIds.delete(buildRemoteTrackKey(participantId, kind));
|
|
1363
|
-
}
|
|
1364
|
-
}
|
|
1365
|
-
clearParticipantListeners() {
|
|
1366
|
-
for (const participantId of Array.from(this.participantListeners.keys())) {
|
|
1367
|
-
this.detachParticipant(participantId);
|
|
1368
|
-
}
|
|
1369
|
-
this.remoteTrackIds.clear();
|
|
1370
|
-
}
|
|
1371
|
-
syncParticipantTracks(participant) {
|
|
1372
|
-
this.handleRemoteTrackUpdate("audio", participant, participant.audioTrack, participant.audioEnabled === true);
|
|
1373
|
-
this.handleRemoteTrackUpdate("video", participant, participant.videoTrack, participant.videoEnabled === true);
|
|
1374
|
-
this.handleRemoteTrackUpdate("screen", participant, participant.screenShareTracks?.video, participant.screenShareEnabled === true);
|
|
1375
|
-
}
|
|
1376
|
-
handleRemoteTrackUpdate(kind, participant, track, enabled) {
|
|
1377
|
-
const key = buildRemoteTrackKey(participant.id, kind);
|
|
1378
|
-
if (!enabled || !track) {
|
|
1379
|
-
this.remoteTrackIds.delete(key);
|
|
1380
|
-
return;
|
|
1381
|
-
}
|
|
1382
|
-
const previousTrackId = this.remoteTrackIds.get(key);
|
|
1383
|
-
if (previousTrackId === track.id) {
|
|
1384
|
-
return;
|
|
1385
|
-
}
|
|
1386
|
-
this.remoteTrackIds.set(key, track.id);
|
|
1387
|
-
const payload = {
|
|
1388
|
-
kind,
|
|
1389
|
-
track,
|
|
1390
|
-
stream: new MediaStream([track]),
|
|
1391
|
-
trackName: track.id,
|
|
1392
|
-
providerSessionId: participant.id,
|
|
1393
|
-
participantId: participant.id,
|
|
1394
|
-
customParticipantId: participant.customParticipantId,
|
|
1395
|
-
userId: participant.userId
|
|
1396
|
-
};
|
|
1397
|
-
for (const handler of this.remoteTrackHandlers) {
|
|
1398
|
-
handler(payload);
|
|
1399
|
-
}
|
|
1400
|
-
}
|
|
1401
|
-
cleanupMeeting() {
|
|
1402
|
-
const meeting = this.meeting;
|
|
1403
|
-
this.detachParticipantMapListeners();
|
|
1404
|
-
this.clearParticipantListeners();
|
|
1405
|
-
this.meeting = null;
|
|
1406
|
-
this.sessionId = null;
|
|
1407
|
-
this.leaveMeetingSilently(meeting);
|
|
1408
|
-
}
|
|
1409
|
-
assertConnectStillActive(lifecycleVersion, meeting) {
|
|
1410
|
-
if (lifecycleVersion === this.lifecycleVersion) {
|
|
1411
|
-
return;
|
|
1412
|
-
}
|
|
1413
|
-
if (meeting) {
|
|
1414
|
-
this.leaveMeetingSilently(meeting);
|
|
1415
|
-
}
|
|
1416
|
-
throw new Error("Cloudflare media transport was destroyed during connect.");
|
|
1417
|
-
}
|
|
1418
|
-
leaveMeetingSilently(meeting) {
|
|
1419
|
-
if (!meeting) {
|
|
1420
|
-
return;
|
|
1421
|
-
}
|
|
1422
|
-
const leavePromise = meeting.leave?.() ?? meeting.leaveRoom?.();
|
|
1423
|
-
if (leavePromise) {
|
|
1424
|
-
void leavePromise.catch(() => {
|
|
1425
|
-
});
|
|
1426
|
-
}
|
|
1427
|
-
}
|
|
1428
|
-
};
|
|
1429
|
-
|
|
1430
|
-
// src/room-p2p-media.ts
|
|
1431
|
-
var DEFAULT_SIGNAL_PREFIX = "edgebase.media.p2p";
|
|
1432
|
-
var DEFAULT_ICE_SERVERS = [
|
|
1433
|
-
{ urls: "stun:stun.l.google.com:19302" }
|
|
1434
|
-
];
|
|
1435
|
-
var DEFAULT_MEMBER_READY_TIMEOUT_MS = 1e4;
|
|
1436
|
-
function buildTrackKey(memberId, trackId) {
|
|
1437
|
-
return `${memberId}:${trackId}`;
|
|
1438
|
-
}
|
|
1439
|
-
function buildExactDeviceConstraint(deviceId) {
|
|
1440
|
-
return { deviceId: { exact: deviceId } };
|
|
1441
|
-
}
|
|
1442
|
-
function normalizeTrackKind(track) {
|
|
1443
|
-
if (track.kind === "audio") return "audio";
|
|
1444
|
-
if (track.kind === "video") return "video";
|
|
1445
|
-
return null;
|
|
1446
|
-
}
|
|
1447
|
-
function serializeDescription(description) {
|
|
1448
|
-
return {
|
|
1449
|
-
type: description.type,
|
|
1450
|
-
sdp: description.sdp ?? void 0
|
|
1451
|
-
};
|
|
1452
|
-
}
|
|
1453
|
-
function serializeCandidate(candidate) {
|
|
1454
|
-
if ("toJSON" in candidate && typeof candidate.toJSON === "function") {
|
|
1455
|
-
return candidate.toJSON();
|
|
1456
|
-
}
|
|
1457
|
-
return candidate;
|
|
1458
|
-
}
|
|
1459
|
-
function installMessage2(packageName) {
|
|
1460
|
-
return `Install ${packageName} to use React Native P2P media transport. See https://edgebase.fun/docs/room/media`;
|
|
1461
|
-
}
|
|
1462
|
-
var RoomP2PMediaTransport = class {
|
|
1463
|
-
room;
|
|
1464
|
-
options;
|
|
1465
|
-
localTracks = /* @__PURE__ */ new Map();
|
|
1466
|
-
peers = /* @__PURE__ */ new Map();
|
|
1467
|
-
remoteTrackHandlers = [];
|
|
1468
|
-
remoteTrackKinds = /* @__PURE__ */ new Map();
|
|
1469
|
-
emittedRemoteTracks = /* @__PURE__ */ new Set();
|
|
1470
|
-
pendingRemoteTracks = /* @__PURE__ */ new Map();
|
|
1471
|
-
subscriptions = [];
|
|
1472
|
-
localMemberId = null;
|
|
1473
|
-
connected = false;
|
|
1474
|
-
runtimePromise = null;
|
|
1475
|
-
constructor(room, options) {
|
|
1476
|
-
this.room = room;
|
|
1477
|
-
this.options = {
|
|
1478
|
-
rtcConfiguration: {
|
|
1479
|
-
...options?.rtcConfiguration,
|
|
1480
|
-
iceServers: options?.rtcConfiguration?.iceServers && options.rtcConfiguration.iceServers.length > 0 ? options.rtcConfiguration.iceServers : DEFAULT_ICE_SERVERS
|
|
1481
|
-
},
|
|
1482
|
-
peerConnectionFactory: options?.peerConnectionFactory,
|
|
1483
|
-
mediaDevices: options?.mediaDevices,
|
|
1484
|
-
signalPrefix: options?.signalPrefix ?? DEFAULT_SIGNAL_PREFIX
|
|
1485
|
-
};
|
|
1486
|
-
}
|
|
1487
|
-
getSessionId() {
|
|
1488
|
-
return this.localMemberId;
|
|
1489
|
-
}
|
|
1490
|
-
getPeerConnection() {
|
|
1491
|
-
if (this.peers.size !== 1) {
|
|
1492
|
-
return null;
|
|
1493
|
-
}
|
|
1494
|
-
return this.peers.values().next().value?.pc ?? null;
|
|
1495
|
-
}
|
|
1496
|
-
async connect(payload) {
|
|
1497
|
-
if (this.connected && this.localMemberId) {
|
|
1498
|
-
return this.localMemberId;
|
|
1499
|
-
}
|
|
1500
|
-
if (payload && typeof payload === "object" && "sessionDescription" in payload) {
|
|
1501
|
-
throw new Error(
|
|
1502
|
-
"RoomP2PMediaTransport.connect() does not accept sessionDescription; use room.signals through the built-in transport instead."
|
|
1503
|
-
);
|
|
1504
|
-
}
|
|
1505
|
-
await this.ensureRuntime();
|
|
1506
|
-
const currentMember = await this.waitForCurrentMember();
|
|
1507
|
-
if (!currentMember) {
|
|
1508
|
-
throw new Error("Join the room before connecting a P2P media transport.");
|
|
1509
|
-
}
|
|
1510
|
-
this.localMemberId = currentMember.memberId;
|
|
1511
|
-
this.connected = true;
|
|
1512
|
-
this.hydrateRemoteTrackKinds();
|
|
1513
|
-
this.attachRoomSubscriptions();
|
|
1514
|
-
try {
|
|
1515
|
-
for (const member of this.room.members.list()) {
|
|
1516
|
-
if (member.memberId !== this.localMemberId) {
|
|
1517
|
-
this.ensurePeer(member.memberId);
|
|
1518
|
-
}
|
|
1519
|
-
}
|
|
1520
|
-
} catch (error) {
|
|
1521
|
-
this.rollbackConnectedState();
|
|
1522
|
-
throw error;
|
|
1523
|
-
}
|
|
1524
|
-
return this.localMemberId;
|
|
1525
|
-
}
|
|
1526
|
-
async waitForCurrentMember(timeoutMs = DEFAULT_MEMBER_READY_TIMEOUT_MS) {
|
|
1527
|
-
const startedAt = Date.now();
|
|
1528
|
-
while (Date.now() - startedAt < timeoutMs) {
|
|
1529
|
-
const member = this.room.members.current();
|
|
1530
|
-
if (member) {
|
|
1531
|
-
return member;
|
|
1532
|
-
}
|
|
1533
|
-
await new Promise((resolve) => globalThis.setTimeout(resolve, 50));
|
|
1534
|
-
}
|
|
1535
|
-
return this.room.members.current();
|
|
1536
|
-
}
|
|
1537
|
-
async enableAudio(constraints = true) {
|
|
1538
|
-
const track = await this.createUserMediaTrack("audio", constraints);
|
|
1539
|
-
if (!track) {
|
|
1540
|
-
throw new Error("P2P transport could not create a local audio track.");
|
|
1541
|
-
}
|
|
1542
|
-
const providerSessionId = await this.ensureConnectedMemberId();
|
|
1543
|
-
this.rememberLocalTrack("audio", track, track.getSettings().deviceId, true);
|
|
1544
|
-
await this.room.media.audio.enable?.({
|
|
1545
|
-
trackId: track.id,
|
|
1546
|
-
deviceId: track.getSettings().deviceId,
|
|
1547
|
-
providerSessionId
|
|
1548
|
-
});
|
|
1549
|
-
this.syncAllPeerSenders();
|
|
1550
|
-
return track;
|
|
1551
|
-
}
|
|
1552
|
-
async enableVideo(constraints = true) {
|
|
1553
|
-
const track = await this.createUserMediaTrack("video", constraints);
|
|
1554
|
-
if (!track) {
|
|
1555
|
-
throw new Error("P2P transport could not create a local video track.");
|
|
1556
|
-
}
|
|
1557
|
-
const providerSessionId = await this.ensureConnectedMemberId();
|
|
1558
|
-
this.rememberLocalTrack("video", track, track.getSettings().deviceId, true);
|
|
1559
|
-
await this.room.media.video.enable?.({
|
|
1560
|
-
trackId: track.id,
|
|
1561
|
-
deviceId: track.getSettings().deviceId,
|
|
1562
|
-
providerSessionId
|
|
1563
|
-
});
|
|
1564
|
-
this.syncAllPeerSenders();
|
|
1565
|
-
return track;
|
|
1566
|
-
}
|
|
1567
|
-
async startScreenShare(constraints = { video: true, audio: false }) {
|
|
1568
|
-
const devices = await this.resolveMediaDevices();
|
|
1569
|
-
if (!devices?.getDisplayMedia) {
|
|
1570
|
-
throw new Error("Screen sharing is not available in this environment.");
|
|
1571
|
-
}
|
|
1572
|
-
const stream = await devices.getDisplayMedia(constraints);
|
|
1573
|
-
const track = stream.getVideoTracks()[0] ?? null;
|
|
1574
|
-
if (!track) {
|
|
1575
|
-
throw new Error("P2P transport could not create a screen-share track.");
|
|
1576
|
-
}
|
|
1577
|
-
track.addEventListener("ended", () => {
|
|
1578
|
-
void this.stopScreenShare();
|
|
1579
|
-
}, { once: true });
|
|
1580
|
-
const providerSessionId = await this.ensureConnectedMemberId();
|
|
1581
|
-
this.rememberLocalTrack("screen", track, track.getSettings().deviceId, true);
|
|
1582
|
-
await this.room.media.screen.start?.({
|
|
1583
|
-
trackId: track.id,
|
|
1584
|
-
deviceId: track.getSettings().deviceId,
|
|
1585
|
-
providerSessionId
|
|
1586
|
-
});
|
|
1587
|
-
this.syncAllPeerSenders();
|
|
1588
|
-
return track;
|
|
1589
|
-
}
|
|
1590
|
-
async disableAudio() {
|
|
1591
|
-
this.releaseLocalTrack("audio");
|
|
1592
|
-
this.syncAllPeerSenders();
|
|
1593
|
-
await this.room.media.audio.disable();
|
|
1594
|
-
}
|
|
1595
|
-
async disableVideo() {
|
|
1596
|
-
this.releaseLocalTrack("video");
|
|
1597
|
-
this.syncAllPeerSenders();
|
|
1598
|
-
await this.room.media.video.disable();
|
|
1599
|
-
}
|
|
1600
|
-
async stopScreenShare() {
|
|
1601
|
-
this.releaseLocalTrack("screen");
|
|
1602
|
-
this.syncAllPeerSenders();
|
|
1603
|
-
await this.room.media.screen.stop();
|
|
1604
|
-
}
|
|
1605
|
-
async setMuted(kind, muted) {
|
|
1606
|
-
const localTrack = this.localTracks.get(kind)?.track;
|
|
1607
|
-
if (localTrack) {
|
|
1608
|
-
localTrack.enabled = !muted;
|
|
1609
|
-
}
|
|
1610
|
-
if (kind === "audio") {
|
|
1611
|
-
await this.room.media.audio.setMuted?.(muted);
|
|
1612
|
-
} else {
|
|
1613
|
-
await this.room.media.video.setMuted?.(muted);
|
|
1614
|
-
}
|
|
1615
|
-
}
|
|
1616
|
-
async switchDevices(payload) {
|
|
1617
|
-
if (payload.audioInputId && this.localTracks.has("audio")) {
|
|
1618
|
-
const nextAudioTrack = await this.createUserMediaTrack("audio", buildExactDeviceConstraint(payload.audioInputId));
|
|
1619
|
-
if (nextAudioTrack) {
|
|
1620
|
-
this.rememberLocalTrack("audio", nextAudioTrack, payload.audioInputId, true);
|
|
1621
|
-
}
|
|
1622
|
-
}
|
|
1623
|
-
if (payload.videoInputId && this.localTracks.has("video")) {
|
|
1624
|
-
const nextVideoTrack = await this.createUserMediaTrack("video", buildExactDeviceConstraint(payload.videoInputId));
|
|
1625
|
-
if (nextVideoTrack) {
|
|
1626
|
-
this.rememberLocalTrack("video", nextVideoTrack, payload.videoInputId, true);
|
|
1627
|
-
}
|
|
1628
|
-
}
|
|
1629
|
-
this.syncAllPeerSenders();
|
|
1630
|
-
await this.room.media.devices.switch(payload);
|
|
1631
|
-
}
|
|
1632
|
-
onRemoteTrack(handler) {
|
|
1633
|
-
this.remoteTrackHandlers.push(handler);
|
|
1634
|
-
return core.createSubscription(() => {
|
|
1635
|
-
const index = this.remoteTrackHandlers.indexOf(handler);
|
|
1636
|
-
if (index >= 0) {
|
|
1637
|
-
this.remoteTrackHandlers.splice(index, 1);
|
|
1638
|
-
}
|
|
1639
|
-
});
|
|
1640
|
-
}
|
|
1641
|
-
destroy() {
|
|
1642
|
-
this.connected = false;
|
|
1643
|
-
this.localMemberId = null;
|
|
1644
|
-
for (const subscription of this.subscriptions.splice(0)) {
|
|
1645
|
-
subscription.unsubscribe();
|
|
1646
|
-
}
|
|
1647
|
-
for (const peer of this.peers.values()) {
|
|
1648
|
-
this.destroyPeer(peer);
|
|
1649
|
-
}
|
|
1650
|
-
this.peers.clear();
|
|
1651
|
-
for (const kind of Array.from(this.localTracks.keys())) {
|
|
1652
|
-
this.releaseLocalTrack(kind);
|
|
1653
|
-
}
|
|
1654
|
-
this.remoteTrackKinds.clear();
|
|
1655
|
-
this.emittedRemoteTracks.clear();
|
|
1656
|
-
this.pendingRemoteTracks.clear();
|
|
1657
|
-
}
|
|
1658
|
-
attachRoomSubscriptions() {
|
|
1659
|
-
if (this.subscriptions.length > 0) {
|
|
1660
|
-
return;
|
|
1661
|
-
}
|
|
1662
|
-
this.subscriptions.push(
|
|
1663
|
-
this.room.members.onJoin((member) => {
|
|
1664
|
-
if (member.memberId !== this.localMemberId) {
|
|
1665
|
-
this.ensurePeer(member.memberId);
|
|
1666
|
-
}
|
|
1667
|
-
}),
|
|
1668
|
-
this.room.members.onSync((members) => {
|
|
1669
|
-
const activeMemberIds = /* @__PURE__ */ new Set();
|
|
1670
|
-
for (const member of members) {
|
|
1671
|
-
if (member.memberId !== this.localMemberId) {
|
|
1672
|
-
activeMemberIds.add(member.memberId);
|
|
1673
|
-
this.ensurePeer(member.memberId);
|
|
1674
|
-
}
|
|
1675
|
-
}
|
|
1676
|
-
for (const memberId of Array.from(this.peers.keys())) {
|
|
1677
|
-
if (!activeMemberIds.has(memberId)) {
|
|
1678
|
-
this.removeRemoteMember(memberId);
|
|
1679
|
-
}
|
|
1680
|
-
}
|
|
1681
|
-
}),
|
|
1682
|
-
this.room.members.onLeave((member) => {
|
|
1683
|
-
this.removeRemoteMember(member.memberId);
|
|
1684
|
-
}),
|
|
1685
|
-
this.room.signals.on(this.offerEvent, (payload, meta) => {
|
|
1686
|
-
void this.handleDescriptionSignal("offer", payload, meta);
|
|
1687
|
-
}),
|
|
1688
|
-
this.room.signals.on(this.answerEvent, (payload, meta) => {
|
|
1689
|
-
void this.handleDescriptionSignal("answer", payload, meta);
|
|
1690
|
-
}),
|
|
1691
|
-
this.room.signals.on(this.iceEvent, (payload, meta) => {
|
|
1692
|
-
void this.handleIceSignal(payload, meta);
|
|
1693
|
-
}),
|
|
1694
|
-
this.room.media.onTrack((track, member) => {
|
|
1695
|
-
if (member.memberId !== this.localMemberId) {
|
|
1696
|
-
this.ensurePeer(member.memberId);
|
|
1697
|
-
}
|
|
1698
|
-
this.rememberRemoteTrackKind(track, member);
|
|
1699
|
-
}),
|
|
1700
|
-
this.room.media.onTrackRemoved((track, member) => {
|
|
1701
|
-
if (!track.trackId) return;
|
|
1702
|
-
const key = buildTrackKey(member.memberId, track.trackId);
|
|
1703
|
-
this.remoteTrackKinds.delete(key);
|
|
1704
|
-
this.emittedRemoteTracks.delete(key);
|
|
1705
|
-
this.pendingRemoteTracks.delete(key);
|
|
1706
|
-
})
|
|
1707
|
-
);
|
|
1708
|
-
}
|
|
1709
|
-
hydrateRemoteTrackKinds() {
|
|
1710
|
-
this.remoteTrackKinds.clear();
|
|
1711
|
-
this.emittedRemoteTracks.clear();
|
|
1712
|
-
this.pendingRemoteTracks.clear();
|
|
1713
|
-
for (const mediaMember of this.room.media.list()) {
|
|
1714
|
-
for (const track of mediaMember.tracks) {
|
|
1715
|
-
this.rememberRemoteTrackKind(track, mediaMember.member);
|
|
1716
|
-
}
|
|
1717
|
-
}
|
|
1718
|
-
}
|
|
1719
|
-
rememberRemoteTrackKind(track, member) {
|
|
1720
|
-
if (!track.trackId || member.memberId === this.localMemberId) {
|
|
1721
|
-
return;
|
|
1722
|
-
}
|
|
1723
|
-
const key = buildTrackKey(member.memberId, track.trackId);
|
|
1724
|
-
this.remoteTrackKinds.set(key, track.kind);
|
|
1725
|
-
const pending = this.pendingRemoteTracks.get(key);
|
|
1726
|
-
if (pending) {
|
|
1727
|
-
this.pendingRemoteTracks.delete(key);
|
|
1728
|
-
this.emitRemoteTrack(member.memberId, pending.track, pending.stream, track.kind);
|
|
1729
|
-
return;
|
|
1730
|
-
}
|
|
1731
|
-
this.flushPendingRemoteTracks(member.memberId, track.kind);
|
|
1732
|
-
}
|
|
1733
|
-
ensurePeer(memberId) {
|
|
1734
|
-
const existing = this.peers.get(memberId);
|
|
1735
|
-
if (existing) {
|
|
1736
|
-
this.syncPeerSenders(existing);
|
|
1737
|
-
return existing;
|
|
1738
|
-
}
|
|
1739
|
-
const factory = this.options.peerConnectionFactory ?? ((configuration) => new RTCPeerConnection(configuration));
|
|
1740
|
-
const pc = factory(this.options.rtcConfiguration);
|
|
1741
|
-
const peer = {
|
|
1742
|
-
memberId,
|
|
1743
|
-
pc,
|
|
1744
|
-
polite: !!this.localMemberId && this.localMemberId.localeCompare(memberId) > 0,
|
|
1745
|
-
makingOffer: false,
|
|
1746
|
-
ignoreOffer: false,
|
|
1747
|
-
isSettingRemoteAnswerPending: false,
|
|
1748
|
-
pendingCandidates: [],
|
|
1749
|
-
senders: /* @__PURE__ */ new Map()
|
|
1750
|
-
};
|
|
1751
|
-
pc.onicecandidate = (event) => {
|
|
1752
|
-
if (!event.candidate) return;
|
|
1753
|
-
void this.room.signals.sendTo(memberId, this.iceEvent, {
|
|
1754
|
-
candidate: serializeCandidate(event.candidate)
|
|
1755
|
-
});
|
|
1756
|
-
};
|
|
1757
|
-
pc.onnegotiationneeded = () => {
|
|
1758
|
-
void this.negotiatePeer(peer);
|
|
1759
|
-
};
|
|
1760
|
-
pc.ontrack = (event) => {
|
|
1761
|
-
const stream = event.streams[0] ?? new MediaStream([event.track]);
|
|
1762
|
-
const key = buildTrackKey(memberId, event.track.id);
|
|
1763
|
-
const exactKind = this.remoteTrackKinds.get(key);
|
|
1764
|
-
const fallbackKind = exactKind ? null : this.resolveFallbackRemoteTrackKind(memberId, event.track);
|
|
1765
|
-
const kind = exactKind ?? fallbackKind ?? normalizeTrackKind(event.track);
|
|
1766
|
-
if (!kind || !exactKind && !fallbackKind && kind === "video" && event.track.kind === "video") {
|
|
1767
|
-
this.pendingRemoteTracks.set(key, { memberId, track: event.track, stream });
|
|
1768
|
-
return;
|
|
1769
|
-
}
|
|
1770
|
-
this.emitRemoteTrack(memberId, event.track, stream, kind);
|
|
1771
|
-
};
|
|
1772
|
-
this.peers.set(memberId, peer);
|
|
1773
|
-
this.syncPeerSenders(peer);
|
|
1774
|
-
return peer;
|
|
1775
|
-
}
|
|
1776
|
-
async negotiatePeer(peer) {
|
|
1777
|
-
if (!this.connected || peer.pc.connectionState === "closed" || peer.makingOffer || peer.isSettingRemoteAnswerPending || peer.pc.signalingState !== "stable") {
|
|
1778
|
-
return;
|
|
1779
|
-
}
|
|
1780
|
-
try {
|
|
1781
|
-
peer.makingOffer = true;
|
|
1782
|
-
await peer.pc.setLocalDescription();
|
|
1783
|
-
if (!peer.pc.localDescription) {
|
|
1784
|
-
return;
|
|
1785
|
-
}
|
|
1786
|
-
await this.room.signals.sendTo(peer.memberId, this.offerEvent, {
|
|
1787
|
-
description: serializeDescription(peer.pc.localDescription)
|
|
1788
|
-
});
|
|
1789
|
-
} catch (error) {
|
|
1790
|
-
console.warn("[RoomP2PMediaTransport] Failed to negotiate peer offer.", {
|
|
1791
|
-
memberId: peer.memberId,
|
|
1792
|
-
signalingState: peer.pc.signalingState,
|
|
1793
|
-
error
|
|
1794
|
-
});
|
|
1795
|
-
} finally {
|
|
1796
|
-
peer.makingOffer = false;
|
|
1797
|
-
}
|
|
1798
|
-
}
|
|
1799
|
-
async handleDescriptionSignal(expectedType, payload, meta) {
|
|
1800
|
-
const senderId = typeof meta.memberId === "string" && meta.memberId.trim() ? meta.memberId.trim() : "";
|
|
1801
|
-
if (!senderId || senderId === this.localMemberId) {
|
|
1802
|
-
return;
|
|
1803
|
-
}
|
|
1804
|
-
const description = this.normalizeDescription(payload);
|
|
1805
|
-
if (!description || description.type !== expectedType) {
|
|
1806
|
-
return;
|
|
1807
|
-
}
|
|
1808
|
-
const peer = this.ensurePeer(senderId);
|
|
1809
|
-
const readyForOffer = !peer.makingOffer && (peer.pc.signalingState === "stable" || peer.isSettingRemoteAnswerPending);
|
|
1810
|
-
const offerCollision = description.type === "offer" && !readyForOffer;
|
|
1811
|
-
peer.ignoreOffer = !peer.polite && offerCollision;
|
|
1812
|
-
if (peer.ignoreOffer) {
|
|
1813
|
-
return;
|
|
1814
|
-
}
|
|
1815
|
-
try {
|
|
1816
|
-
peer.isSettingRemoteAnswerPending = description.type === "answer";
|
|
1817
|
-
await peer.pc.setRemoteDescription(description);
|
|
1818
|
-
peer.isSettingRemoteAnswerPending = false;
|
|
1819
|
-
await this.flushPendingCandidates(peer);
|
|
1820
|
-
if (description.type === "offer") {
|
|
1821
|
-
this.syncPeerSenders(peer);
|
|
1822
|
-
await peer.pc.setLocalDescription();
|
|
1823
|
-
if (!peer.pc.localDescription) {
|
|
1824
|
-
return;
|
|
1825
|
-
}
|
|
1826
|
-
await this.room.signals.sendTo(senderId, this.answerEvent, {
|
|
1827
|
-
description: serializeDescription(peer.pc.localDescription)
|
|
1828
|
-
});
|
|
1829
|
-
}
|
|
1830
|
-
} catch (error) {
|
|
1831
|
-
console.warn("[RoomP2PMediaTransport] Failed to apply remote session description.", {
|
|
1832
|
-
memberId: senderId,
|
|
1833
|
-
expectedType,
|
|
1834
|
-
signalingState: peer.pc.signalingState,
|
|
1835
|
-
error
|
|
1836
|
-
});
|
|
1837
|
-
peer.isSettingRemoteAnswerPending = false;
|
|
1838
|
-
}
|
|
1839
|
-
}
|
|
1840
|
-
async handleIceSignal(payload, meta) {
|
|
1841
|
-
const senderId = typeof meta.memberId === "string" && meta.memberId.trim() ? meta.memberId.trim() : "";
|
|
1842
|
-
if (!senderId || senderId === this.localMemberId) {
|
|
1843
|
-
return;
|
|
1844
|
-
}
|
|
1845
|
-
const candidate = this.normalizeCandidate(payload);
|
|
1846
|
-
if (!candidate) {
|
|
1847
|
-
return;
|
|
1848
|
-
}
|
|
1849
|
-
const peer = this.ensurePeer(senderId);
|
|
1850
|
-
if (!peer.pc.remoteDescription) {
|
|
1851
|
-
peer.pendingCandidates.push(candidate);
|
|
1852
|
-
return;
|
|
1853
|
-
}
|
|
1854
|
-
try {
|
|
1855
|
-
await peer.pc.addIceCandidate(candidate);
|
|
1856
|
-
} catch (error) {
|
|
1857
|
-
console.warn("[RoomP2PMediaTransport] Failed to add ICE candidate.", {
|
|
1858
|
-
memberId: senderId,
|
|
1859
|
-
error
|
|
1860
|
-
});
|
|
1861
|
-
if (!peer.ignoreOffer) {
|
|
1862
|
-
peer.pendingCandidates.push(candidate);
|
|
1863
|
-
}
|
|
1864
|
-
}
|
|
1865
|
-
}
|
|
1866
|
-
async flushPendingCandidates(peer) {
|
|
1867
|
-
if (!peer.pc.remoteDescription || peer.pendingCandidates.length === 0) {
|
|
1868
|
-
return;
|
|
1869
|
-
}
|
|
1870
|
-
const pending = [...peer.pendingCandidates];
|
|
1871
|
-
peer.pendingCandidates.length = 0;
|
|
1872
|
-
for (const candidate of pending) {
|
|
1873
|
-
try {
|
|
1874
|
-
await peer.pc.addIceCandidate(candidate);
|
|
1875
|
-
} catch (error) {
|
|
1876
|
-
console.warn("[RoomP2PMediaTransport] Failed to flush pending ICE candidate.", {
|
|
1877
|
-
memberId: peer.memberId,
|
|
1878
|
-
error
|
|
1879
|
-
});
|
|
1880
|
-
if (!peer.ignoreOffer) {
|
|
1881
|
-
peer.pendingCandidates.push(candidate);
|
|
1882
|
-
}
|
|
1883
|
-
}
|
|
1884
|
-
}
|
|
1885
|
-
}
|
|
1886
|
-
syncAllPeerSenders() {
|
|
1887
|
-
for (const peer of this.peers.values()) {
|
|
1888
|
-
this.syncPeerSenders(peer);
|
|
1889
|
-
}
|
|
1890
|
-
}
|
|
1891
|
-
syncPeerSenders(peer) {
|
|
1892
|
-
const activeKinds = /* @__PURE__ */ new Set();
|
|
1893
|
-
let changed = false;
|
|
1894
|
-
for (const [kind, localTrack] of this.localTracks.entries()) {
|
|
1895
|
-
activeKinds.add(kind);
|
|
1896
|
-
const sender = peer.senders.get(kind);
|
|
1897
|
-
if (sender) {
|
|
1898
|
-
if (sender.track !== localTrack.track) {
|
|
1899
|
-
void sender.replaceTrack(localTrack.track);
|
|
1900
|
-
changed = true;
|
|
1901
|
-
}
|
|
1902
|
-
continue;
|
|
1903
|
-
}
|
|
1904
|
-
const addedSender = peer.pc.addTrack(localTrack.track, new MediaStream([localTrack.track]));
|
|
1905
|
-
peer.senders.set(kind, addedSender);
|
|
1906
|
-
changed = true;
|
|
1907
|
-
}
|
|
1908
|
-
for (const [kind, sender] of Array.from(peer.senders.entries())) {
|
|
1909
|
-
if (activeKinds.has(kind)) {
|
|
1910
|
-
continue;
|
|
1911
|
-
}
|
|
1912
|
-
try {
|
|
1913
|
-
peer.pc.removeTrack(sender);
|
|
1914
|
-
} catch {
|
|
1915
|
-
}
|
|
1916
|
-
peer.senders.delete(kind);
|
|
1917
|
-
changed = true;
|
|
1918
|
-
}
|
|
1919
|
-
if (changed) {
|
|
1920
|
-
void this.negotiatePeer(peer);
|
|
1921
|
-
}
|
|
1922
|
-
}
|
|
1923
|
-
emitRemoteTrack(memberId, track, stream, kind) {
|
|
1924
|
-
const key = buildTrackKey(memberId, track.id);
|
|
1925
|
-
if (this.emittedRemoteTracks.has(key)) {
|
|
1926
|
-
return;
|
|
1927
|
-
}
|
|
1928
|
-
this.emittedRemoteTracks.add(key);
|
|
1929
|
-
this.remoteTrackKinds.set(key, kind);
|
|
1930
|
-
const participant = this.findMember(memberId);
|
|
1931
|
-
const payload = {
|
|
1932
|
-
kind,
|
|
1933
|
-
track,
|
|
1934
|
-
stream,
|
|
1935
|
-
trackName: track.id,
|
|
1936
|
-
providerSessionId: memberId,
|
|
1937
|
-
participantId: memberId,
|
|
1938
|
-
userId: participant?.userId
|
|
1939
|
-
};
|
|
1940
|
-
for (const handler of this.remoteTrackHandlers) {
|
|
1941
|
-
handler(payload);
|
|
1942
|
-
}
|
|
1943
|
-
}
|
|
1944
|
-
resolveFallbackRemoteTrackKind(memberId, track) {
|
|
1945
|
-
const normalizedKind = normalizeTrackKind(track);
|
|
1946
|
-
if (!normalizedKind) {
|
|
1947
|
-
return null;
|
|
1948
|
-
}
|
|
1949
|
-
if (normalizedKind === "audio") {
|
|
1950
|
-
return "audio";
|
|
1951
|
-
}
|
|
1952
|
-
return this.getNextUnassignedPublishedVideoLikeKind(memberId);
|
|
1953
|
-
}
|
|
1954
|
-
flushPendingRemoteTracks(memberId, roomKind) {
|
|
1955
|
-
const expectedTrackKind = roomKind === "audio" ? "audio" : "video";
|
|
1956
|
-
for (const [key, pending] of this.pendingRemoteTracks.entries()) {
|
|
1957
|
-
if (pending.memberId !== memberId || pending.track.kind !== expectedTrackKind) {
|
|
1958
|
-
continue;
|
|
1959
|
-
}
|
|
1960
|
-
this.pendingRemoteTracks.delete(key);
|
|
1961
|
-
this.emitRemoteTrack(memberId, pending.track, pending.stream, roomKind);
|
|
1962
|
-
return;
|
|
1963
|
-
}
|
|
1964
|
-
}
|
|
1965
|
-
getPublishedVideoLikeKinds(memberId) {
|
|
1966
|
-
const mediaMember = this.room.media.list().find((entry) => entry.member.memberId === memberId);
|
|
1967
|
-
if (!mediaMember) {
|
|
1968
|
-
return [];
|
|
1969
|
-
}
|
|
1970
|
-
const publishedKinds = /* @__PURE__ */ new Set();
|
|
1971
|
-
for (const track of mediaMember.tracks) {
|
|
1972
|
-
if ((track.kind === "video" || track.kind === "screen") && track.trackId) {
|
|
1973
|
-
publishedKinds.add(track.kind);
|
|
1974
|
-
}
|
|
1975
|
-
}
|
|
1976
|
-
return Array.from(publishedKinds);
|
|
1977
|
-
}
|
|
1978
|
-
getNextUnassignedPublishedVideoLikeKind(memberId) {
|
|
1979
|
-
const publishedKinds = this.getPublishedVideoLikeKinds(memberId);
|
|
1980
|
-
if (publishedKinds.length === 0) {
|
|
1981
|
-
return null;
|
|
1982
|
-
}
|
|
1983
|
-
const assignedKinds = /* @__PURE__ */ new Set();
|
|
1984
|
-
for (const key of this.emittedRemoteTracks) {
|
|
1985
|
-
if (!key.startsWith(`${memberId}:`)) {
|
|
1986
|
-
continue;
|
|
1987
|
-
}
|
|
1988
|
-
const kind = this.remoteTrackKinds.get(key);
|
|
1989
|
-
if (kind === "video" || kind === "screen") {
|
|
1990
|
-
assignedKinds.add(kind);
|
|
1991
|
-
}
|
|
1992
|
-
}
|
|
1993
|
-
return publishedKinds.find((kind) => !assignedKinds.has(kind)) ?? null;
|
|
1994
|
-
}
|
|
1995
|
-
closePeer(memberId) {
|
|
1996
|
-
const peer = this.peers.get(memberId);
|
|
1997
|
-
if (!peer) return;
|
|
1998
|
-
this.destroyPeer(peer);
|
|
1999
|
-
this.peers.delete(memberId);
|
|
2000
|
-
}
|
|
2001
|
-
removeRemoteMember(memberId) {
|
|
2002
|
-
this.remoteTrackKinds.forEach((_kind, key) => {
|
|
2003
|
-
if (key.startsWith(`${memberId}:`)) {
|
|
2004
|
-
this.remoteTrackKinds.delete(key);
|
|
2005
|
-
}
|
|
2006
|
-
});
|
|
2007
|
-
this.emittedRemoteTracks.forEach((key) => {
|
|
2008
|
-
if (key.startsWith(`${memberId}:`)) {
|
|
2009
|
-
this.emittedRemoteTracks.delete(key);
|
|
2010
|
-
}
|
|
2011
|
-
});
|
|
2012
|
-
this.pendingRemoteTracks.forEach((_pending, key) => {
|
|
2013
|
-
if (key.startsWith(`${memberId}:`)) {
|
|
2014
|
-
this.pendingRemoteTracks.delete(key);
|
|
2015
|
-
}
|
|
2016
|
-
});
|
|
2017
|
-
this.closePeer(memberId);
|
|
2018
|
-
}
|
|
2019
|
-
findMember(memberId) {
|
|
2020
|
-
return this.room.members.list().find((member) => member.memberId === memberId);
|
|
2021
|
-
}
|
|
2022
|
-
rollbackConnectedState() {
|
|
2023
|
-
this.connected = false;
|
|
2024
|
-
this.localMemberId = null;
|
|
2025
|
-
for (const subscription of this.subscriptions.splice(0)) {
|
|
2026
|
-
subscription.unsubscribe();
|
|
2027
|
-
}
|
|
2028
|
-
for (const peer of this.peers.values()) {
|
|
2029
|
-
this.destroyPeer(peer);
|
|
2030
|
-
}
|
|
2031
|
-
this.peers.clear();
|
|
2032
|
-
this.remoteTrackKinds.clear();
|
|
2033
|
-
this.emittedRemoteTracks.clear();
|
|
2034
|
-
this.pendingRemoteTracks.clear();
|
|
2035
|
-
}
|
|
2036
|
-
destroyPeer(peer) {
|
|
2037
|
-
peer.pc.onicecandidate = null;
|
|
2038
|
-
peer.pc.onnegotiationneeded = null;
|
|
2039
|
-
peer.pc.ontrack = null;
|
|
2040
|
-
try {
|
|
2041
|
-
peer.pc.close();
|
|
2042
|
-
} catch {
|
|
2043
|
-
}
|
|
2044
|
-
}
|
|
2045
|
-
async createUserMediaTrack(kind, constraints) {
|
|
2046
|
-
const devices = await this.resolveMediaDevices();
|
|
2047
|
-
if (!devices?.getUserMedia || constraints === false) {
|
|
2048
|
-
return null;
|
|
2049
|
-
}
|
|
2050
|
-
const stream = await devices.getUserMedia(
|
|
2051
|
-
kind === "audio" ? { audio: constraints, video: false } : { audio: false, video: constraints }
|
|
2052
|
-
);
|
|
2053
|
-
return kind === "audio" ? stream.getAudioTracks()[0] ?? null : stream.getVideoTracks()[0] ?? null;
|
|
2054
|
-
}
|
|
2055
|
-
rememberLocalTrack(kind, track, deviceId, stopOnCleanup) {
|
|
2056
|
-
this.releaseLocalTrack(kind);
|
|
2057
|
-
this.localTracks.set(kind, {
|
|
2058
|
-
kind,
|
|
2059
|
-
track,
|
|
2060
|
-
deviceId,
|
|
2061
|
-
stopOnCleanup
|
|
2062
|
-
});
|
|
2063
|
-
}
|
|
2064
|
-
releaseLocalTrack(kind) {
|
|
2065
|
-
const local = this.localTracks.get(kind);
|
|
2066
|
-
if (!local) return;
|
|
2067
|
-
if (local.stopOnCleanup) {
|
|
2068
|
-
local.track.stop();
|
|
2069
|
-
}
|
|
2070
|
-
this.localTracks.delete(kind);
|
|
2071
|
-
}
|
|
2072
|
-
async ensureConnectedMemberId() {
|
|
2073
|
-
if (this.localMemberId) {
|
|
2074
|
-
return this.localMemberId;
|
|
2075
|
-
}
|
|
2076
|
-
return this.connect();
|
|
2077
|
-
}
|
|
2078
|
-
normalizeDescription(payload) {
|
|
2079
|
-
if (!payload || typeof payload !== "object") {
|
|
2080
|
-
return null;
|
|
2081
|
-
}
|
|
2082
|
-
const raw = payload.description;
|
|
2083
|
-
if (!raw || typeof raw.type !== "string") {
|
|
2084
|
-
return null;
|
|
2085
|
-
}
|
|
2086
|
-
return {
|
|
2087
|
-
type: raw.type,
|
|
2088
|
-
sdp: typeof raw.sdp === "string" ? raw.sdp : void 0
|
|
2089
|
-
};
|
|
2090
|
-
}
|
|
2091
|
-
normalizeCandidate(payload) {
|
|
2092
|
-
if (!payload || typeof payload !== "object") {
|
|
2093
|
-
return null;
|
|
2094
|
-
}
|
|
2095
|
-
const raw = payload.candidate;
|
|
2096
|
-
if (!raw || typeof raw.candidate !== "string") {
|
|
2097
|
-
return null;
|
|
2098
|
-
}
|
|
2099
|
-
return raw;
|
|
2100
|
-
}
|
|
2101
|
-
async ensureRuntime() {
|
|
2102
|
-
this.runtimePromise ??= import('@cloudflare/react-native-webrtc').then((mod) => {
|
|
2103
|
-
const runtime = mod;
|
|
2104
|
-
runtime.registerGlobals?.();
|
|
2105
|
-
return runtime;
|
|
2106
|
-
}).catch((error) => {
|
|
2107
|
-
throw new Error(`${installMessage2("@cloudflare/react-native-webrtc")}
|
|
2108
|
-
${String(error)}`);
|
|
2109
|
-
});
|
|
2110
|
-
return this.runtimePromise;
|
|
2111
|
-
}
|
|
2112
|
-
async resolveMediaDevices() {
|
|
2113
|
-
if (this.options.mediaDevices) {
|
|
2114
|
-
return this.options.mediaDevices;
|
|
2115
|
-
}
|
|
2116
|
-
const runtime = await this.ensureRuntime();
|
|
2117
|
-
return runtime.mediaDevices ?? (typeof navigator !== "undefined" ? navigator.mediaDevices : void 0);
|
|
2118
|
-
}
|
|
2119
|
-
get offerEvent() {
|
|
2120
|
-
return `${this.options.signalPrefix}.offer`;
|
|
2121
|
-
}
|
|
2122
|
-
get answerEvent() {
|
|
2123
|
-
return `${this.options.signalPrefix}.answer`;
|
|
2124
|
-
}
|
|
2125
|
-
get iceEvent() {
|
|
2126
|
-
return `${this.options.signalPrefix}.ice`;
|
|
2127
|
-
}
|
|
2128
|
-
};
|
|
2129
|
-
|
|
2130
|
-
// src/room.ts
|
|
2131
1025
|
var UNSAFE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
2132
1026
|
function deepSet(obj, path, value) {
|
|
2133
1027
|
const parts = path.split(".");
|
|
@@ -2165,18 +1059,21 @@ function cloneRecord(value) {
|
|
|
2165
1059
|
var WS_CONNECTING = 0;
|
|
2166
1060
|
var WS_OPEN = 1;
|
|
2167
1061
|
var ROOM_EXPLICIT_LEAVE_CLOSE_CODE = 4005;
|
|
1062
|
+
var ROOM_AUTH_STATE_LOST_CLOSE_CODE = 4006;
|
|
2168
1063
|
var ROOM_EXPLICIT_LEAVE_REASON = "Client left room";
|
|
2169
|
-
var
|
|
1064
|
+
var ROOM_HEARTBEAT_INTERVAL_MS = 8e3;
|
|
1065
|
+
var ROOM_HEARTBEAT_STALE_TIMEOUT_MS = 2e4;
|
|
2170
1066
|
function isSocketOpenOrConnecting(socket) {
|
|
2171
1067
|
return !!socket && (socket.readyState === WS_OPEN || socket.readyState === WS_CONNECTING);
|
|
2172
1068
|
}
|
|
2173
1069
|
function closeSocketAfterLeave(socket, reason) {
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
1070
|
+
try {
|
|
1071
|
+
socket.close(ROOM_EXPLICIT_LEAVE_CLOSE_CODE, reason);
|
|
1072
|
+
} catch {
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
function getDefaultHeartbeatStaleTimeoutMs(heartbeatIntervalMs) {
|
|
1076
|
+
return Math.max(Math.floor(heartbeatIntervalMs * 2.5), ROOM_HEARTBEAT_STALE_TIMEOUT_MS);
|
|
2180
1077
|
}
|
|
2181
1078
|
var RoomClient = class _RoomClient {
|
|
2182
1079
|
baseUrl;
|
|
@@ -2192,7 +1089,7 @@ var RoomClient = class _RoomClient {
|
|
|
2192
1089
|
_playerState = {};
|
|
2193
1090
|
_playerVersion = 0;
|
|
2194
1091
|
_members = [];
|
|
2195
|
-
|
|
1092
|
+
lastLocalMemberState = null;
|
|
2196
1093
|
// ─── Connection ───
|
|
2197
1094
|
ws = null;
|
|
2198
1095
|
reconnectAttempts = 0;
|
|
@@ -2205,16 +1102,41 @@ var RoomClient = class _RoomClient {
|
|
|
2205
1102
|
reconnectInfo = null;
|
|
2206
1103
|
connectingPromise = null;
|
|
2207
1104
|
heartbeatTimer = null;
|
|
1105
|
+
lastHeartbeatAckAt = Date.now();
|
|
1106
|
+
disconnectResetTimer = null;
|
|
2208
1107
|
intentionallyLeft = false;
|
|
2209
1108
|
waitingForAuth = false;
|
|
2210
1109
|
joinRequested = false;
|
|
2211
1110
|
unsubAuthState = null;
|
|
1111
|
+
browserNetworkListenersAttached = false;
|
|
1112
|
+
browserOfflineHandler = () => {
|
|
1113
|
+
if (this.intentionallyLeft || !this.joinRequested) {
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
if (this.connectionState === "connected") {
|
|
1117
|
+
this.setConnectionState("reconnecting");
|
|
1118
|
+
}
|
|
1119
|
+
if (isSocketOpenOrConnecting(this.ws)) {
|
|
1120
|
+
try {
|
|
1121
|
+
this.ws?.close();
|
|
1122
|
+
} catch {
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
};
|
|
1126
|
+
browserOnlineHandler = () => {
|
|
1127
|
+
if (this.intentionallyLeft || !this.joinRequested || this.connectingPromise || isSocketOpenOrConnecting(this.ws)) {
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
if (this.connectionState === "reconnecting" || this.connectionState === "disconnected") {
|
|
1131
|
+
this.ensureConnection().catch(() => {
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
};
|
|
2212
1135
|
// ─── Pending send() requests (requestId → { resolve, reject, timeout }) ───
|
|
2213
1136
|
pendingRequests = /* @__PURE__ */ new Map();
|
|
2214
1137
|
pendingSignalRequests = /* @__PURE__ */ new Map();
|
|
2215
1138
|
pendingAdminRequests = /* @__PURE__ */ new Map();
|
|
2216
1139
|
pendingMemberStateRequests = /* @__PURE__ */ new Map();
|
|
2217
|
-
pendingMediaRequests = /* @__PURE__ */ new Map();
|
|
2218
1140
|
// ─── Subscriptions ───
|
|
2219
1141
|
sharedStateHandlers = [];
|
|
2220
1142
|
playerStateHandlers = [];
|
|
@@ -2224,16 +1146,14 @@ var RoomClient = class _RoomClient {
|
|
|
2224
1146
|
errorHandlers = [];
|
|
2225
1147
|
kickedHandlers = [];
|
|
2226
1148
|
memberSyncHandlers = [];
|
|
1149
|
+
memberSnapshotHandlers = [];
|
|
2227
1150
|
memberJoinHandlers = [];
|
|
2228
1151
|
memberLeaveHandlers = [];
|
|
2229
1152
|
memberStateHandlers = [];
|
|
2230
1153
|
signalHandlers = /* @__PURE__ */ new Map();
|
|
2231
1154
|
anySignalHandlers = [];
|
|
2232
|
-
mediaTrackHandlers = [];
|
|
2233
|
-
mediaTrackRemovedHandlers = [];
|
|
2234
|
-
mediaStateHandlers = [];
|
|
2235
|
-
mediaDeviceHandlers = [];
|
|
2236
1155
|
reconnectHandlers = [];
|
|
1156
|
+
recoveryFailureHandlers = [];
|
|
2237
1157
|
connectionStateHandlers = [];
|
|
2238
1158
|
state = {
|
|
2239
1159
|
getShared: () => this.getSharedState(),
|
|
@@ -2243,7 +1163,8 @@ var RoomClient = class _RoomClient {
|
|
|
2243
1163
|
send: (actionType, payload) => this.send(actionType, payload)
|
|
2244
1164
|
};
|
|
2245
1165
|
meta = {
|
|
2246
|
-
get: () => this.getMetadata()
|
|
1166
|
+
get: () => this.getMetadata(),
|
|
1167
|
+
summary: () => this.getSummary()
|
|
2247
1168
|
};
|
|
2248
1169
|
signals = {
|
|
2249
1170
|
send: (event, payload, options) => this.sendSignal(event, payload, options),
|
|
@@ -2252,7 +1173,7 @@ var RoomClient = class _RoomClient {
|
|
|
2252
1173
|
onAny: (handler) => this.onAnySignal(handler)
|
|
2253
1174
|
};
|
|
2254
1175
|
members = {
|
|
2255
|
-
list: () =>
|
|
1176
|
+
list: () => this._members.map((member) => cloneValue(member)),
|
|
2256
1177
|
current: () => {
|
|
2257
1178
|
const connectionId = this.currentConnectionId;
|
|
2258
1179
|
if (connectionId) {
|
|
@@ -2268,7 +1189,9 @@ var RoomClient = class _RoomClient {
|
|
|
2268
1189
|
const member = this._members.find((entry) => entry.userId === userId) ?? null;
|
|
2269
1190
|
return member ? cloneValue(member) : null;
|
|
2270
1191
|
},
|
|
1192
|
+
awaitCurrent: (timeoutMs = 1e4) => this.waitForCurrentMember(timeoutMs),
|
|
2271
1193
|
onSync: (handler) => this.onMembersSync(handler),
|
|
1194
|
+
onSnapshot: (handler) => this.onMemberSnapshot(handler),
|
|
2272
1195
|
onJoin: (handler) => this.onMemberJoin(handler),
|
|
2273
1196
|
onLeave: (handler) => this.onMemberLeave(handler),
|
|
2274
1197
|
setState: (state) => this.sendMemberState(state),
|
|
@@ -2277,53 +1200,16 @@ var RoomClient = class _RoomClient {
|
|
|
2277
1200
|
};
|
|
2278
1201
|
admin = {
|
|
2279
1202
|
kick: (memberId) => this.sendAdmin("kick", memberId),
|
|
2280
|
-
mute: (memberId) => this.sendAdmin("mute", memberId),
|
|
2281
1203
|
block: (memberId) => this.sendAdmin("block", memberId),
|
|
2282
|
-
setRole: (memberId, role) => this.sendAdmin("setRole", memberId, { role })
|
|
2283
|
-
disableVideo: (memberId) => this.sendAdmin("disableVideo", memberId),
|
|
2284
|
-
stopScreenShare: (memberId) => this.sendAdmin("stopScreenShare", memberId)
|
|
2285
|
-
};
|
|
2286
|
-
media = {
|
|
2287
|
-
list: () => cloneValue(this._mediaMembers),
|
|
2288
|
-
audio: {
|
|
2289
|
-
enable: (payload) => this.sendMedia("publish", "audio", payload),
|
|
2290
|
-
disable: () => this.sendMedia("unpublish", "audio"),
|
|
2291
|
-
setMuted: (muted) => this.sendMedia("mute", "audio", { muted })
|
|
2292
|
-
},
|
|
2293
|
-
video: {
|
|
2294
|
-
enable: (payload) => this.sendMedia("publish", "video", payload),
|
|
2295
|
-
disable: () => this.sendMedia("unpublish", "video"),
|
|
2296
|
-
setMuted: (muted) => this.sendMedia("mute", "video", { muted })
|
|
2297
|
-
},
|
|
2298
|
-
screen: {
|
|
2299
|
-
start: (payload) => this.sendMedia("publish", "screen", payload),
|
|
2300
|
-
stop: () => this.sendMedia("unpublish", "screen")
|
|
2301
|
-
},
|
|
2302
|
-
devices: {
|
|
2303
|
-
switch: (payload) => this.switchMediaDevices(payload)
|
|
2304
|
-
},
|
|
2305
|
-
cloudflareRealtimeKit: {
|
|
2306
|
-
createSession: (payload) => this.requestCloudflareRealtimeKitMedia("session", "POST", payload)
|
|
2307
|
-
},
|
|
2308
|
-
transport: (options) => {
|
|
2309
|
-
const provider = options?.provider ?? "cloudflare_realtimekit";
|
|
2310
|
-
if (provider === "p2p") {
|
|
2311
|
-
const p2pOptions = options?.p2p;
|
|
2312
|
-
return new RoomP2PMediaTransport(this, p2pOptions);
|
|
2313
|
-
}
|
|
2314
|
-
const cloudflareOptions = options?.cloudflareRealtimeKit;
|
|
2315
|
-
return new RoomCloudflareMediaTransport(this, cloudflareOptions);
|
|
2316
|
-
},
|
|
2317
|
-
onTrack: (handler) => this.onMediaTrack(handler),
|
|
2318
|
-
onTrackRemoved: (handler) => this.onMediaTrackRemoved(handler),
|
|
2319
|
-
onStateChange: (handler) => this.onMediaStateChange(handler),
|
|
2320
|
-
onDeviceChange: (handler) => this.onMediaDeviceChange(handler)
|
|
1204
|
+
setRole: (memberId, role) => this.sendAdmin("setRole", memberId, { role })
|
|
2321
1205
|
};
|
|
2322
1206
|
session = {
|
|
2323
1207
|
onError: (handler) => this.onError(handler),
|
|
2324
1208
|
onKicked: (handler) => this.onKicked(handler),
|
|
2325
1209
|
onReconnect: (handler) => this.onReconnect(handler),
|
|
2326
|
-
onConnectionStateChange: (handler) => this.onConnectionStateChange(handler)
|
|
1210
|
+
onConnectionStateChange: (handler) => this.onConnectionStateChange(handler),
|
|
1211
|
+
onRecoveryFailure: (handler) => this.onRecoveryFailure(handler),
|
|
1212
|
+
getDebugSnapshot: () => this.getDebugSnapshot()
|
|
2327
1213
|
};
|
|
2328
1214
|
constructor(baseUrl, namespace, roomId, tokenManager, options) {
|
|
2329
1215
|
this.baseUrl = baseUrl;
|
|
@@ -2335,11 +1221,16 @@ var RoomClient = class _RoomClient {
|
|
|
2335
1221
|
maxReconnectAttempts: options?.maxReconnectAttempts ?? 10,
|
|
2336
1222
|
reconnectBaseDelay: options?.reconnectBaseDelay ?? 1e3,
|
|
2337
1223
|
sendTimeout: options?.sendTimeout ?? 1e4,
|
|
2338
|
-
connectionTimeout: options?.connectionTimeout ?? 15e3
|
|
1224
|
+
connectionTimeout: options?.connectionTimeout ?? 15e3,
|
|
1225
|
+
heartbeatIntervalMs: options?.heartbeatIntervalMs ?? ROOM_HEARTBEAT_INTERVAL_MS,
|
|
1226
|
+
heartbeatStaleTimeoutMs: options?.heartbeatStaleTimeoutMs ?? getDefaultHeartbeatStaleTimeoutMs(options?.heartbeatIntervalMs ?? ROOM_HEARTBEAT_INTERVAL_MS),
|
|
1227
|
+
networkRecoveryGraceMs: options?.networkRecoveryGraceMs ?? 3500,
|
|
1228
|
+
disconnectResetTimeoutMs: options?.disconnectResetTimeoutMs ?? 8e3
|
|
2339
1229
|
};
|
|
2340
1230
|
this.unsubAuthState = this.tokenManager.onAuthStateChange((user) => {
|
|
2341
1231
|
this.handleAuthStateChange(user);
|
|
2342
1232
|
});
|
|
1233
|
+
this.attachBrowserNetworkListeners();
|
|
2343
1234
|
}
|
|
2344
1235
|
// ─── State Accessors ───
|
|
2345
1236
|
/** Get current shared state (read-only snapshot) */
|
|
@@ -2350,6 +1241,17 @@ var RoomClient = class _RoomClient {
|
|
|
2350
1241
|
getPlayerState() {
|
|
2351
1242
|
return cloneRecord(this._playerState);
|
|
2352
1243
|
}
|
|
1244
|
+
async waitForCurrentMember(timeoutMs = 1e4) {
|
|
1245
|
+
const startedAt = Date.now();
|
|
1246
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
1247
|
+
const member = this.members.current();
|
|
1248
|
+
if (member) {
|
|
1249
|
+
return member;
|
|
1250
|
+
}
|
|
1251
|
+
await new Promise((resolve) => globalThis.setTimeout(resolve, 50));
|
|
1252
|
+
}
|
|
1253
|
+
return this.members.current();
|
|
1254
|
+
}
|
|
2353
1255
|
// ─── Metadata (HTTP, no WebSocket needed) ───
|
|
2354
1256
|
/**
|
|
2355
1257
|
* Get room metadata without joining (HTTP GET).
|
|
@@ -2358,45 +1260,107 @@ var RoomClient = class _RoomClient {
|
|
|
2358
1260
|
async getMetadata() {
|
|
2359
1261
|
return _RoomClient.getMetadata(this.baseUrl, this.namespace, this.roomId);
|
|
2360
1262
|
}
|
|
2361
|
-
async
|
|
2362
|
-
return this.
|
|
1263
|
+
async getSummary() {
|
|
1264
|
+
return _RoomClient.getSummary(this.baseUrl, this.namespace, this.roomId);
|
|
1265
|
+
}
|
|
1266
|
+
async checkConnection() {
|
|
1267
|
+
return _RoomClient.checkConnection(this.baseUrl, this.namespace, this.roomId);
|
|
1268
|
+
}
|
|
1269
|
+
/**
|
|
1270
|
+
* Static: Get room metadata without creating a RoomClient instance.
|
|
1271
|
+
* Useful for lobby screens where you need room info before joining.
|
|
1272
|
+
*/
|
|
1273
|
+
static async getMetadata(baseUrl, namespace, roomId) {
|
|
1274
|
+
return _RoomClient.requestPublicRoomResource(
|
|
1275
|
+
baseUrl,
|
|
1276
|
+
"metadata",
|
|
1277
|
+
namespace,
|
|
1278
|
+
roomId,
|
|
1279
|
+
"Failed to get room metadata"
|
|
1280
|
+
);
|
|
2363
1281
|
}
|
|
2364
|
-
async
|
|
2365
|
-
|
|
2366
|
-
|
|
1282
|
+
static async getSummary(baseUrl, namespace, roomId) {
|
|
1283
|
+
return _RoomClient.requestPublicRoomResource(
|
|
1284
|
+
baseUrl,
|
|
1285
|
+
"summary",
|
|
1286
|
+
namespace,
|
|
1287
|
+
roomId,
|
|
1288
|
+
"Failed to get room summary"
|
|
2367
1289
|
);
|
|
2368
|
-
|
|
2369
|
-
|
|
1290
|
+
}
|
|
1291
|
+
static async getSummaries(baseUrl, namespace, roomIds) {
|
|
1292
|
+
const url = `${baseUrl.replace(/\/$/, "")}/api/room/summaries`;
|
|
1293
|
+
let res;
|
|
1294
|
+
try {
|
|
1295
|
+
res = await fetch(url, {
|
|
1296
|
+
method: "POST",
|
|
1297
|
+
headers: { "Content-Type": "application/json" },
|
|
1298
|
+
body: JSON.stringify({ namespace, ids: roomIds })
|
|
1299
|
+
});
|
|
1300
|
+
} catch (error) {
|
|
1301
|
+
throw core.networkError(
|
|
1302
|
+
`Failed to get room summaries. Could not reach ${baseUrl.replace(/\/$/, "")}. Make sure the EdgeBase server is running and reachable.`,
|
|
1303
|
+
{ cause: error }
|
|
1304
|
+
);
|
|
2370
1305
|
}
|
|
2371
|
-
const
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
1306
|
+
const data = await res.json().catch(() => null);
|
|
1307
|
+
if (!res.ok) {
|
|
1308
|
+
const parsed = core.parseErrorResponse(res.status, data);
|
|
1309
|
+
parsed.message = `Failed to get room summaries: ${parsed.message}`;
|
|
1310
|
+
throw parsed;
|
|
1311
|
+
}
|
|
1312
|
+
return data;
|
|
1313
|
+
}
|
|
1314
|
+
static async checkConnection(baseUrl, namespace, roomId) {
|
|
1315
|
+
const url = `${baseUrl.replace(/\/$/, "")}/api/room/connect-check?namespace=${encodeURIComponent(namespace)}&id=${encodeURIComponent(roomId)}`;
|
|
1316
|
+
let response;
|
|
1317
|
+
try {
|
|
1318
|
+
response = await fetch(url);
|
|
1319
|
+
} catch (error) {
|
|
1320
|
+
throw core.networkError(
|
|
1321
|
+
`Room connect-check could not reach ${baseUrl.replace(/\/$/, "")}. Make sure the EdgeBase server is running and reachable.`,
|
|
1322
|
+
{ cause: error }
|
|
1323
|
+
);
|
|
1324
|
+
}
|
|
1325
|
+
const data = await response.json().catch(() => null);
|
|
2383
1326
|
if (!response.ok) {
|
|
1327
|
+
throw core.parseErrorResponse(response.status, data);
|
|
1328
|
+
}
|
|
1329
|
+
if (typeof data?.ok !== "boolean" || typeof data?.type !== "string" || typeof data?.category !== "string" || typeof data?.message !== "string") {
|
|
2384
1330
|
throw new core.EdgeBaseError(
|
|
2385
|
-
response.status,
|
|
2386
|
-
|
|
1331
|
+
response.status || 500,
|
|
1332
|
+
"Room connect-check returned an unexpected response. The EdgeBase server and SDK may be out of sync."
|
|
2387
1333
|
);
|
|
2388
1334
|
}
|
|
2389
|
-
|
|
1335
|
+
const diagnostic = data;
|
|
1336
|
+
return {
|
|
1337
|
+
ok: diagnostic.ok,
|
|
1338
|
+
type: diagnostic.type,
|
|
1339
|
+
category: diagnostic.category,
|
|
1340
|
+
message: diagnostic.message,
|
|
1341
|
+
namespace: typeof diagnostic.namespace === "string" ? diagnostic.namespace : void 0,
|
|
1342
|
+
roomId: typeof diagnostic.roomId === "string" ? diagnostic.roomId : void 0,
|
|
1343
|
+
runtime: typeof diagnostic.runtime === "string" ? diagnostic.runtime : void 0,
|
|
1344
|
+
pendingCount: typeof diagnostic.pendingCount === "number" ? diagnostic.pendingCount : void 0,
|
|
1345
|
+
maxPending: typeof diagnostic.maxPending === "number" ? diagnostic.maxPending : void 0
|
|
1346
|
+
};
|
|
2390
1347
|
}
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
1348
|
+
static async requestPublicRoomResource(baseUrl, resource, namespace, roomId, failureMessage) {
|
|
1349
|
+
const url = `${baseUrl.replace(/\/$/, "")}/api/room/${resource}?namespace=${encodeURIComponent(namespace)}&id=${encodeURIComponent(roomId)}`;
|
|
1350
|
+
let res;
|
|
1351
|
+
try {
|
|
1352
|
+
res = await fetch(url);
|
|
1353
|
+
} catch (error) {
|
|
1354
|
+
throw core.networkError(
|
|
1355
|
+
`${failureMessage}. Could not reach ${baseUrl.replace(/\/$/, "")}. Make sure the EdgeBase server is running and reachable.`,
|
|
1356
|
+
{ cause: error }
|
|
1357
|
+
);
|
|
1358
|
+
}
|
|
2398
1359
|
if (!res.ok) {
|
|
2399
|
-
|
|
1360
|
+
const data = await res.json().catch(() => null);
|
|
1361
|
+
const parsed = core.parseErrorResponse(res.status, data);
|
|
1362
|
+
parsed.message = `${failureMessage}: ${parsed.message}`;
|
|
1363
|
+
throw parsed;
|
|
2400
1364
|
}
|
|
2401
1365
|
return res.json();
|
|
2402
1366
|
}
|
|
@@ -2417,15 +1381,8 @@ var RoomClient = class _RoomClient {
|
|
|
2417
1381
|
this.joinRequested = false;
|
|
2418
1382
|
this.waitingForAuth = false;
|
|
2419
1383
|
this.stopHeartbeat();
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
pending.reject(new core.EdgeBaseError(499, "Room left"));
|
|
2423
|
-
}
|
|
2424
|
-
this.pendingRequests.clear();
|
|
2425
|
-
this.rejectPendingVoidRequests(this.pendingSignalRequests, new core.EdgeBaseError(499, "Room left"));
|
|
2426
|
-
this.rejectPendingVoidRequests(this.pendingAdminRequests, new core.EdgeBaseError(499, "Room left"));
|
|
2427
|
-
this.rejectPendingVoidRequests(this.pendingMemberStateRequests, new core.EdgeBaseError(499, "Room left"));
|
|
2428
|
-
this.rejectPendingVoidRequests(this.pendingMediaRequests, new core.EdgeBaseError(499, "Room left"));
|
|
1384
|
+
this.clearDisconnectResetTimer();
|
|
1385
|
+
this.rejectAllPendingRequests(new core.EdgeBaseError(499, "Room left"));
|
|
2429
1386
|
if (this.ws) {
|
|
2430
1387
|
const socket = this.ws;
|
|
2431
1388
|
this.sendRaw({ type: "leave" });
|
|
@@ -2441,15 +1398,28 @@ var RoomClient = class _RoomClient {
|
|
|
2441
1398
|
this._playerState = {};
|
|
2442
1399
|
this._playerVersion = 0;
|
|
2443
1400
|
this._members = [];
|
|
2444
|
-
this.
|
|
1401
|
+
this.lastLocalMemberState = null;
|
|
2445
1402
|
this.currentUserId = null;
|
|
2446
1403
|
this.currentConnectionId = null;
|
|
2447
1404
|
this.reconnectInfo = null;
|
|
2448
1405
|
this.setConnectionState("disconnected");
|
|
2449
1406
|
}
|
|
2450
|
-
|
|
1407
|
+
assertConnected(action) {
|
|
1408
|
+
if (!this.ws || !this.connected || !this.authenticated) {
|
|
1409
|
+
throw new core.EdgeBaseError(
|
|
1410
|
+
400,
|
|
1411
|
+
`Room connection required before ${action}. Call room.join() and wait for the connection to finish.`
|
|
1412
|
+
);
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
/**
|
|
1416
|
+
* Destroy the RoomClient and release all resources.
|
|
1417
|
+
* Calls leave() if still connected, unsubscribes from auth state changes,
|
|
1418
|
+
* and clears all handler arrays to allow garbage collection.
|
|
1419
|
+
*/
|
|
2451
1420
|
destroy() {
|
|
2452
1421
|
this.leave();
|
|
1422
|
+
this.detachBrowserNetworkListeners();
|
|
2453
1423
|
this.unsubAuthState?.();
|
|
2454
1424
|
this.unsubAuthState = null;
|
|
2455
1425
|
this.sharedStateHandlers.length = 0;
|
|
@@ -2464,10 +1434,6 @@ var RoomClient = class _RoomClient {
|
|
|
2464
1434
|
this.memberStateHandlers.length = 0;
|
|
2465
1435
|
this.signalHandlers.clear();
|
|
2466
1436
|
this.anySignalHandlers.length = 0;
|
|
2467
|
-
this.mediaTrackHandlers.length = 0;
|
|
2468
|
-
this.mediaTrackRemovedHandlers.length = 0;
|
|
2469
|
-
this.mediaStateHandlers.length = 0;
|
|
2470
|
-
this.mediaDeviceHandlers.length = 0;
|
|
2471
1437
|
this.reconnectHandlers.length = 0;
|
|
2472
1438
|
this.connectionStateHandlers.length = 0;
|
|
2473
1439
|
}
|
|
@@ -2480,9 +1446,7 @@ var RoomClient = class _RoomClient {
|
|
|
2480
1446
|
* const result = await room.send('SET_SCORE', { score: 42 });
|
|
2481
1447
|
*/
|
|
2482
1448
|
async send(actionType, payload) {
|
|
2483
|
-
|
|
2484
|
-
throw new core.EdgeBaseError(400, "Not connected to room");
|
|
2485
|
-
}
|
|
1449
|
+
this.assertConnected(`sending action '${actionType}'`);
|
|
2486
1450
|
const requestId = generateRequestId();
|
|
2487
1451
|
return new Promise((resolve, reject) => {
|
|
2488
1452
|
const timeout = setTimeout(() => {
|
|
@@ -2600,6 +1564,13 @@ var RoomClient = class _RoomClient {
|
|
|
2600
1564
|
if (index >= 0) this.memberSyncHandlers.splice(index, 1);
|
|
2601
1565
|
});
|
|
2602
1566
|
}
|
|
1567
|
+
onMemberSnapshot(handler) {
|
|
1568
|
+
this.memberSnapshotHandlers.push(handler);
|
|
1569
|
+
return core.createSubscription(() => {
|
|
1570
|
+
const index = this.memberSnapshotHandlers.indexOf(handler);
|
|
1571
|
+
if (index >= 0) this.memberSnapshotHandlers.splice(index, 1);
|
|
1572
|
+
});
|
|
1573
|
+
}
|
|
2603
1574
|
onMemberJoin(handler) {
|
|
2604
1575
|
this.memberJoinHandlers.push(handler);
|
|
2605
1576
|
return core.createSubscription(() => {
|
|
@@ -2628,6 +1599,13 @@ var RoomClient = class _RoomClient {
|
|
|
2628
1599
|
if (index >= 0) this.reconnectHandlers.splice(index, 1);
|
|
2629
1600
|
});
|
|
2630
1601
|
}
|
|
1602
|
+
onRecoveryFailure(handler) {
|
|
1603
|
+
this.recoveryFailureHandlers.push(handler);
|
|
1604
|
+
return core.createSubscription(() => {
|
|
1605
|
+
const index = this.recoveryFailureHandlers.indexOf(handler);
|
|
1606
|
+
if (index >= 0) this.recoveryFailureHandlers.splice(index, 1);
|
|
1607
|
+
});
|
|
1608
|
+
}
|
|
2631
1609
|
onConnectionStateChange(handler) {
|
|
2632
1610
|
this.connectionStateHandlers.push(handler);
|
|
2633
1611
|
return core.createSubscription(() => {
|
|
@@ -2635,38 +1613,8 @@ var RoomClient = class _RoomClient {
|
|
|
2635
1613
|
if (index >= 0) this.connectionStateHandlers.splice(index, 1);
|
|
2636
1614
|
});
|
|
2637
1615
|
}
|
|
2638
|
-
onMediaTrack(handler) {
|
|
2639
|
-
this.mediaTrackHandlers.push(handler);
|
|
2640
|
-
return core.createSubscription(() => {
|
|
2641
|
-
const index = this.mediaTrackHandlers.indexOf(handler);
|
|
2642
|
-
if (index >= 0) this.mediaTrackHandlers.splice(index, 1);
|
|
2643
|
-
});
|
|
2644
|
-
}
|
|
2645
|
-
onMediaTrackRemoved(handler) {
|
|
2646
|
-
this.mediaTrackRemovedHandlers.push(handler);
|
|
2647
|
-
return core.createSubscription(() => {
|
|
2648
|
-
const index = this.mediaTrackRemovedHandlers.indexOf(handler);
|
|
2649
|
-
if (index >= 0) this.mediaTrackRemovedHandlers.splice(index, 1);
|
|
2650
|
-
});
|
|
2651
|
-
}
|
|
2652
|
-
onMediaStateChange(handler) {
|
|
2653
|
-
this.mediaStateHandlers.push(handler);
|
|
2654
|
-
return core.createSubscription(() => {
|
|
2655
|
-
const index = this.mediaStateHandlers.indexOf(handler);
|
|
2656
|
-
if (index >= 0) this.mediaStateHandlers.splice(index, 1);
|
|
2657
|
-
});
|
|
2658
|
-
}
|
|
2659
|
-
onMediaDeviceChange(handler) {
|
|
2660
|
-
this.mediaDeviceHandlers.push(handler);
|
|
2661
|
-
return core.createSubscription(() => {
|
|
2662
|
-
const index = this.mediaDeviceHandlers.indexOf(handler);
|
|
2663
|
-
if (index >= 0) this.mediaDeviceHandlers.splice(index, 1);
|
|
2664
|
-
});
|
|
2665
|
-
}
|
|
2666
1616
|
async sendSignal(event, payload, options) {
|
|
2667
|
-
|
|
2668
|
-
throw new core.EdgeBaseError(400, "Not connected to room");
|
|
2669
|
-
}
|
|
1617
|
+
this.assertConnected(`sending signal '${event}'`);
|
|
2670
1618
|
const requestId = generateRequestId();
|
|
2671
1619
|
return new Promise((resolve, reject) => {
|
|
2672
1620
|
const timeout = setTimeout(() => {
|
|
@@ -2685,34 +1633,39 @@ var RoomClient = class _RoomClient {
|
|
|
2685
1633
|
});
|
|
2686
1634
|
}
|
|
2687
1635
|
async sendMemberState(state) {
|
|
1636
|
+
const nextState = {
|
|
1637
|
+
...this.lastLocalMemberState ?? {},
|
|
1638
|
+
...cloneRecord(state)
|
|
1639
|
+
};
|
|
2688
1640
|
return this.sendMemberStateRequest({
|
|
2689
1641
|
type: "member_state",
|
|
2690
1642
|
state
|
|
1643
|
+
}, () => {
|
|
1644
|
+
this.lastLocalMemberState = nextState;
|
|
2691
1645
|
});
|
|
2692
1646
|
}
|
|
2693
1647
|
async clearMemberState() {
|
|
1648
|
+
const clearedState = {};
|
|
2694
1649
|
return this.sendMemberStateRequest({
|
|
2695
1650
|
type: "member_state_clear"
|
|
1651
|
+
}, () => {
|
|
1652
|
+
this.lastLocalMemberState = clearedState;
|
|
2696
1653
|
});
|
|
2697
1654
|
}
|
|
2698
|
-
async sendMemberStateRequest(payload) {
|
|
2699
|
-
|
|
2700
|
-
throw new core.EdgeBaseError(400, "Not connected to room");
|
|
2701
|
-
}
|
|
1655
|
+
async sendMemberStateRequest(payload, onSuccess) {
|
|
1656
|
+
this.assertConnected("updating member state");
|
|
2702
1657
|
const requestId = generateRequestId();
|
|
2703
1658
|
return new Promise((resolve, reject) => {
|
|
2704
1659
|
const timeout = setTimeout(() => {
|
|
2705
1660
|
this.pendingMemberStateRequests.delete(requestId);
|
|
2706
1661
|
reject(new core.EdgeBaseError(408, "Member state update timed out"));
|
|
2707
1662
|
}, this.options.sendTimeout);
|
|
2708
|
-
this.pendingMemberStateRequests.set(requestId, { resolve, reject, timeout });
|
|
1663
|
+
this.pendingMemberStateRequests.set(requestId, { resolve, reject, timeout, onSuccess });
|
|
2709
1664
|
this.sendRaw({ ...payload, requestId });
|
|
2710
1665
|
});
|
|
2711
1666
|
}
|
|
2712
1667
|
async sendAdmin(operation, memberId, payload) {
|
|
2713
|
-
|
|
2714
|
-
throw new core.EdgeBaseError(400, "Not connected to room");
|
|
2715
|
-
}
|
|
1668
|
+
this.assertConnected(`running admin operation '${operation}'`);
|
|
2716
1669
|
const requestId = generateRequestId();
|
|
2717
1670
|
return new Promise((resolve, reject) => {
|
|
2718
1671
|
const timeout = setTimeout(() => {
|
|
@@ -2729,39 +1682,6 @@ var RoomClient = class _RoomClient {
|
|
|
2729
1682
|
});
|
|
2730
1683
|
});
|
|
2731
1684
|
}
|
|
2732
|
-
async sendMedia(operation, kind, payload) {
|
|
2733
|
-
if (!this.ws || !this.connected || !this.authenticated) {
|
|
2734
|
-
throw new core.EdgeBaseError(400, "Not connected to room");
|
|
2735
|
-
}
|
|
2736
|
-
const requestId = generateRequestId();
|
|
2737
|
-
return new Promise((resolve, reject) => {
|
|
2738
|
-
const timeout = setTimeout(() => {
|
|
2739
|
-
this.pendingMediaRequests.delete(requestId);
|
|
2740
|
-
reject(new core.EdgeBaseError(408, `Media operation '${operation}' timed out`));
|
|
2741
|
-
}, this.options.sendTimeout);
|
|
2742
|
-
this.pendingMediaRequests.set(requestId, { resolve, reject, timeout });
|
|
2743
|
-
this.sendRaw({
|
|
2744
|
-
type: "media",
|
|
2745
|
-
operation,
|
|
2746
|
-
kind,
|
|
2747
|
-
payload: payload ?? {},
|
|
2748
|
-
requestId
|
|
2749
|
-
});
|
|
2750
|
-
});
|
|
2751
|
-
}
|
|
2752
|
-
async switchMediaDevices(payload) {
|
|
2753
|
-
const operations = [];
|
|
2754
|
-
if (payload.audioInputId) {
|
|
2755
|
-
operations.push(this.sendMedia("device", "audio", { deviceId: payload.audioInputId }));
|
|
2756
|
-
}
|
|
2757
|
-
if (payload.videoInputId) {
|
|
2758
|
-
operations.push(this.sendMedia("device", "video", { deviceId: payload.videoInputId }));
|
|
2759
|
-
}
|
|
2760
|
-
if (payload.screenInputId) {
|
|
2761
|
-
operations.push(this.sendMedia("device", "screen", { deviceId: payload.screenInputId }));
|
|
2762
|
-
}
|
|
2763
|
-
await Promise.all(operations);
|
|
2764
|
-
}
|
|
2765
1685
|
// ─── Private: Connection ───
|
|
2766
1686
|
async establishConnection() {
|
|
2767
1687
|
return new Promise((resolve, reject) => {
|
|
@@ -2809,11 +1729,16 @@ var RoomClient = class _RoomClient {
|
|
|
2809
1729
|
this.joined = false;
|
|
2810
1730
|
this.ws = null;
|
|
2811
1731
|
this.stopHeartbeat();
|
|
2812
|
-
|
|
2813
|
-
|
|
1732
|
+
const closeMessage = event.reason?.trim() ? `Room authentication lost: ${event.reason}` : "Room authentication lost";
|
|
1733
|
+
const closeError = event.code === ROOM_AUTH_STATE_LOST_CLOSE_CODE ? new core.EdgeBaseError(401, closeMessage) : new core.EdgeBaseError(499, "WebSocket connection lost");
|
|
1734
|
+
if (event.code === ROOM_AUTH_STATE_LOST_CLOSE_CODE && this.connectionState !== "auth_lost") {
|
|
1735
|
+
this.setConnectionState("auth_lost");
|
|
2814
1736
|
}
|
|
2815
1737
|
if (!this.intentionallyLeft) {
|
|
2816
|
-
this.rejectAllPendingRequests(
|
|
1738
|
+
this.rejectAllPendingRequests(closeError);
|
|
1739
|
+
}
|
|
1740
|
+
if (event.code === 4004 && this.connectionState !== "kicked") {
|
|
1741
|
+
this.handleKicked();
|
|
2817
1742
|
}
|
|
2818
1743
|
if (!this.intentionallyLeft && !this.waitingForAuth && this.options.autoReconnect && this.reconnectAttempts < this.options.maxReconnectAttempts) {
|
|
2819
1744
|
this.scheduleReconnect();
|
|
@@ -2847,11 +1772,19 @@ var RoomClient = class _RoomClient {
|
|
|
2847
1772
|
(refreshToken) => refreshAccessToken(this.baseUrl, refreshToken)
|
|
2848
1773
|
);
|
|
2849
1774
|
if (!token) {
|
|
2850
|
-
throw new core.EdgeBaseError(
|
|
1775
|
+
throw new core.EdgeBaseError(
|
|
1776
|
+
401,
|
|
1777
|
+
"Room authentication requires a signed-in session. Sign in before joining the room."
|
|
1778
|
+
);
|
|
2851
1779
|
}
|
|
2852
1780
|
return new Promise((resolve, reject) => {
|
|
2853
1781
|
const timeout = setTimeout(() => {
|
|
2854
|
-
reject(
|
|
1782
|
+
reject(
|
|
1783
|
+
new core.EdgeBaseError(
|
|
1784
|
+
401,
|
|
1785
|
+
"Room authentication timed out. Check the server auth response and room connectivity."
|
|
1786
|
+
)
|
|
1787
|
+
);
|
|
2855
1788
|
}, 1e4);
|
|
2856
1789
|
const originalOnMessage = this.ws?.onmessage;
|
|
2857
1790
|
if (this.ws) {
|
|
@@ -2868,7 +1801,8 @@ var RoomClient = class _RoomClient {
|
|
|
2868
1801
|
lastSharedState: this._sharedState,
|
|
2869
1802
|
lastSharedVersion: this._sharedVersion,
|
|
2870
1803
|
lastPlayerState: this._playerState,
|
|
2871
|
-
lastPlayerVersion: this._playerVersion
|
|
1804
|
+
lastPlayerVersion: this._playerVersion,
|
|
1805
|
+
lastMemberState: this.getReconnectMemberState()
|
|
2872
1806
|
});
|
|
2873
1807
|
this.joined = true;
|
|
2874
1808
|
resolve();
|
|
@@ -2925,9 +1859,6 @@ var RoomClient = class _RoomClient {
|
|
|
2925
1859
|
case "members_sync":
|
|
2926
1860
|
this.handleMembersSync(msg);
|
|
2927
1861
|
break;
|
|
2928
|
-
case "media_sync":
|
|
2929
|
-
this.handleMediaSync(msg);
|
|
2930
|
-
break;
|
|
2931
1862
|
case "member_join":
|
|
2932
1863
|
this.handleMemberJoinFrame(msg);
|
|
2933
1864
|
break;
|
|
@@ -2940,24 +1871,6 @@ var RoomClient = class _RoomClient {
|
|
|
2940
1871
|
case "member_state_error":
|
|
2941
1872
|
this.handleMemberStateError(msg);
|
|
2942
1873
|
break;
|
|
2943
|
-
case "media_track":
|
|
2944
|
-
this.handleMediaTrackFrame(msg);
|
|
2945
|
-
break;
|
|
2946
|
-
case "media_track_removed":
|
|
2947
|
-
this.handleMediaTrackRemovedFrame(msg);
|
|
2948
|
-
break;
|
|
2949
|
-
case "media_state":
|
|
2950
|
-
this.handleMediaStateFrame(msg);
|
|
2951
|
-
break;
|
|
2952
|
-
case "media_device":
|
|
2953
|
-
this.handleMediaDeviceFrame(msg);
|
|
2954
|
-
break;
|
|
2955
|
-
case "media_result":
|
|
2956
|
-
this.handleMediaResult(msg);
|
|
2957
|
-
break;
|
|
2958
|
-
case "media_error":
|
|
2959
|
-
this.handleMediaError(msg);
|
|
2960
|
-
break;
|
|
2961
1874
|
case "admin_result":
|
|
2962
1875
|
this.handleAdminResult(msg);
|
|
2963
1876
|
break;
|
|
@@ -2970,6 +1883,9 @@ var RoomClient = class _RoomClient {
|
|
|
2970
1883
|
case "error":
|
|
2971
1884
|
this.handleError(msg);
|
|
2972
1885
|
break;
|
|
1886
|
+
case "pong":
|
|
1887
|
+
this.lastHeartbeatAckAt = Date.now();
|
|
1888
|
+
break;
|
|
2973
1889
|
}
|
|
2974
1890
|
}
|
|
2975
1891
|
handleSync(msg) {
|
|
@@ -3085,22 +2001,18 @@ var RoomClient = class _RoomClient {
|
|
|
3085
2001
|
handleMembersSync(msg) {
|
|
3086
2002
|
const members = this.normalizeMembers(msg.members);
|
|
3087
2003
|
this._members = members;
|
|
3088
|
-
|
|
3089
|
-
this.syncMediaMemberInfo(member);
|
|
3090
|
-
}
|
|
3091
|
-
const snapshot = cloneValue(this._members);
|
|
2004
|
+
const snapshot = this._members.map((member) => cloneValue(member));
|
|
3092
2005
|
for (const handler of this.memberSyncHandlers) {
|
|
3093
2006
|
handler(snapshot);
|
|
3094
2007
|
}
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
2008
|
+
for (const handler of this.memberSnapshotHandlers) {
|
|
2009
|
+
handler(snapshot);
|
|
2010
|
+
}
|
|
3098
2011
|
}
|
|
3099
2012
|
handleMemberJoinFrame(msg) {
|
|
3100
2013
|
const member = this.normalizeMember(msg.member);
|
|
3101
2014
|
if (!member) return;
|
|
3102
2015
|
this.upsertMember(member);
|
|
3103
|
-
this.syncMediaMemberInfo(member);
|
|
3104
2016
|
const snapshot = cloneValue(member);
|
|
3105
2017
|
for (const handler of this.memberJoinHandlers) {
|
|
3106
2018
|
handler(snapshot);
|
|
@@ -3110,7 +2022,6 @@ var RoomClient = class _RoomClient {
|
|
|
3110
2022
|
const member = this.normalizeMember(msg.member);
|
|
3111
2023
|
if (!member) return;
|
|
3112
2024
|
this.removeMember(member.memberId);
|
|
3113
|
-
this.removeMediaMember(member.memberId);
|
|
3114
2025
|
const reason = this.normalizeLeaveReason(msg.reason);
|
|
3115
2026
|
const snapshot = cloneValue(member);
|
|
3116
2027
|
for (const handler of this.memberLeaveHandlers) {
|
|
@@ -3123,13 +2034,13 @@ var RoomClient = class _RoomClient {
|
|
|
3123
2034
|
if (!member) return;
|
|
3124
2035
|
member.state = state;
|
|
3125
2036
|
this.upsertMember(member);
|
|
3126
|
-
this.syncMediaMemberInfo(member);
|
|
3127
2037
|
const requestId = msg.requestId;
|
|
3128
|
-
if (requestId
|
|
2038
|
+
if (requestId) {
|
|
3129
2039
|
const pending = this.pendingMemberStateRequests.get(requestId);
|
|
3130
2040
|
if (pending) {
|
|
3131
2041
|
clearTimeout(pending.timeout);
|
|
3132
2042
|
this.pendingMemberStateRequests.delete(requestId);
|
|
2043
|
+
pending.onSuccess?.();
|
|
3133
2044
|
pending.resolve();
|
|
3134
2045
|
}
|
|
3135
2046
|
}
|
|
@@ -3148,93 +2059,6 @@ var RoomClient = class _RoomClient {
|
|
|
3148
2059
|
this.pendingMemberStateRequests.delete(requestId);
|
|
3149
2060
|
pending.reject(new core.EdgeBaseError(400, msg.message || "Member state update failed"));
|
|
3150
2061
|
}
|
|
3151
|
-
handleMediaTrackFrame(msg) {
|
|
3152
|
-
const member = this.normalizeMember(msg.member);
|
|
3153
|
-
const track = this.normalizeMediaTrack(msg.track);
|
|
3154
|
-
if (!member || !track) return;
|
|
3155
|
-
const mediaMember = this.ensureMediaMember(member);
|
|
3156
|
-
this.upsertMediaTrack(mediaMember, track);
|
|
3157
|
-
this.mergeMediaState(mediaMember, track.kind, {
|
|
3158
|
-
published: true,
|
|
3159
|
-
muted: track.muted,
|
|
3160
|
-
trackId: track.trackId,
|
|
3161
|
-
deviceId: track.deviceId,
|
|
3162
|
-
publishedAt: track.publishedAt,
|
|
3163
|
-
adminDisabled: track.adminDisabled
|
|
3164
|
-
});
|
|
3165
|
-
const memberSnapshot = cloneValue(mediaMember.member);
|
|
3166
|
-
const trackSnapshot = cloneValue(track);
|
|
3167
|
-
for (const handler of this.mediaTrackHandlers) {
|
|
3168
|
-
handler(trackSnapshot, memberSnapshot);
|
|
3169
|
-
}
|
|
3170
|
-
}
|
|
3171
|
-
handleMediaTrackRemovedFrame(msg) {
|
|
3172
|
-
const member = this.normalizeMember(msg.member);
|
|
3173
|
-
const track = this.normalizeMediaTrack(msg.track);
|
|
3174
|
-
if (!member || !track) return;
|
|
3175
|
-
const mediaMember = this.ensureMediaMember(member);
|
|
3176
|
-
this.removeMediaTrack(mediaMember, track);
|
|
3177
|
-
mediaMember.state = {
|
|
3178
|
-
...mediaMember.state,
|
|
3179
|
-
[track.kind]: {
|
|
3180
|
-
published: false,
|
|
3181
|
-
muted: false,
|
|
3182
|
-
adminDisabled: false
|
|
3183
|
-
}
|
|
3184
|
-
};
|
|
3185
|
-
const memberSnapshot = cloneValue(mediaMember.member);
|
|
3186
|
-
const trackSnapshot = cloneValue(track);
|
|
3187
|
-
for (const handler of this.mediaTrackRemovedHandlers) {
|
|
3188
|
-
handler(trackSnapshot, memberSnapshot);
|
|
3189
|
-
}
|
|
3190
|
-
}
|
|
3191
|
-
handleMediaStateFrame(msg) {
|
|
3192
|
-
const member = this.normalizeMember(msg.member);
|
|
3193
|
-
if (!member) return;
|
|
3194
|
-
const mediaMember = this.ensureMediaMember(member);
|
|
3195
|
-
mediaMember.state = this.normalizeMediaState(msg.state);
|
|
3196
|
-
const memberSnapshot = cloneValue(mediaMember.member);
|
|
3197
|
-
const stateSnapshot = cloneValue(mediaMember.state);
|
|
3198
|
-
for (const handler of this.mediaStateHandlers) {
|
|
3199
|
-
handler(memberSnapshot, stateSnapshot);
|
|
3200
|
-
}
|
|
3201
|
-
}
|
|
3202
|
-
handleMediaDeviceFrame(msg) {
|
|
3203
|
-
const member = this.normalizeMember(msg.member);
|
|
3204
|
-
const kind = this.normalizeMediaKind(msg.kind);
|
|
3205
|
-
const deviceId = typeof msg.deviceId === "string" ? msg.deviceId : "";
|
|
3206
|
-
if (!member || !kind || !deviceId) return;
|
|
3207
|
-
const mediaMember = this.ensureMediaMember(member);
|
|
3208
|
-
this.mergeMediaState(mediaMember, kind, { deviceId });
|
|
3209
|
-
for (const track of mediaMember.tracks) {
|
|
3210
|
-
if (track.kind === kind) {
|
|
3211
|
-
track.deviceId = deviceId;
|
|
3212
|
-
}
|
|
3213
|
-
}
|
|
3214
|
-
const memberSnapshot = cloneValue(mediaMember.member);
|
|
3215
|
-
const change = { kind, deviceId };
|
|
3216
|
-
for (const handler of this.mediaDeviceHandlers) {
|
|
3217
|
-
handler(memberSnapshot, change);
|
|
3218
|
-
}
|
|
3219
|
-
}
|
|
3220
|
-
handleMediaResult(msg) {
|
|
3221
|
-
const requestId = msg.requestId;
|
|
3222
|
-
if (!requestId) return;
|
|
3223
|
-
const pending = this.pendingMediaRequests.get(requestId);
|
|
3224
|
-
if (!pending) return;
|
|
3225
|
-
clearTimeout(pending.timeout);
|
|
3226
|
-
this.pendingMediaRequests.delete(requestId);
|
|
3227
|
-
pending.resolve();
|
|
3228
|
-
}
|
|
3229
|
-
handleMediaError(msg) {
|
|
3230
|
-
const requestId = msg.requestId;
|
|
3231
|
-
if (!requestId) return;
|
|
3232
|
-
const pending = this.pendingMediaRequests.get(requestId);
|
|
3233
|
-
if (!pending) return;
|
|
3234
|
-
clearTimeout(pending.timeout);
|
|
3235
|
-
this.pendingMediaRequests.delete(requestId);
|
|
3236
|
-
pending.reject(new core.EdgeBaseError(400, msg.message || "Media operation failed"));
|
|
3237
|
-
}
|
|
3238
2062
|
handleAdminResult(msg) {
|
|
3239
2063
|
const requestId = msg.requestId;
|
|
3240
2064
|
if (!requestId) return;
|
|
@@ -3260,8 +2084,13 @@ var RoomClient = class _RoomClient {
|
|
|
3260
2084
|
this.setConnectionState("kicked");
|
|
3261
2085
|
}
|
|
3262
2086
|
handleError(msg) {
|
|
2087
|
+
const code = typeof msg.code === "string" ? msg.code : "";
|
|
2088
|
+
const message = typeof msg.message === "string" ? msg.message : "";
|
|
2089
|
+
if (this.shouldTreatErrorAsAuthLoss(code)) {
|
|
2090
|
+
this.handleRoomAuthStateLoss(message);
|
|
2091
|
+
}
|
|
3263
2092
|
for (const handler of this.errorHandlers) {
|
|
3264
|
-
handler({ code
|
|
2093
|
+
handler({ code, message });
|
|
3265
2094
|
}
|
|
3266
2095
|
}
|
|
3267
2096
|
refreshAuth() {
|
|
@@ -3270,9 +2099,6 @@ var RoomClient = class _RoomClient {
|
|
|
3270
2099
|
this.sendRaw({ type: "auth", token });
|
|
3271
2100
|
}
|
|
3272
2101
|
handleAuthStateChange(user) {
|
|
3273
|
-
if (user === null) {
|
|
3274
|
-
this.rejectAllPendingRequests(new core.EdgeBaseError(401, "Auth state lost"));
|
|
3275
|
-
}
|
|
3276
2102
|
if (user) {
|
|
3277
2103
|
if (this.ws && this.connected && this.authenticated) {
|
|
3278
2104
|
this.refreshAuth();
|
|
@@ -3289,6 +2115,7 @@ var RoomClient = class _RoomClient {
|
|
|
3289
2115
|
this.waitingForAuth = this.joinRequested;
|
|
3290
2116
|
this.reconnectInfo = null;
|
|
3291
2117
|
this.setConnectionState("auth_lost");
|
|
2118
|
+
this.rejectAllPendingRequests(new core.EdgeBaseError(401, "Auth state lost"));
|
|
3292
2119
|
if (this.ws) {
|
|
3293
2120
|
const socket = this.ws;
|
|
3294
2121
|
this.sendRaw({ type: "leave" });
|
|
@@ -3297,7 +2124,6 @@ var RoomClient = class _RoomClient {
|
|
|
3297
2124
|
this.connected = false;
|
|
3298
2125
|
this.authenticated = false;
|
|
3299
2126
|
this.joined = false;
|
|
3300
|
-
this._mediaMembers = [];
|
|
3301
2127
|
this.currentUserId = null;
|
|
3302
2128
|
this.currentConnectionId = null;
|
|
3303
2129
|
try {
|
|
@@ -3309,7 +2135,6 @@ var RoomClient = class _RoomClient {
|
|
|
3309
2135
|
this.connected = false;
|
|
3310
2136
|
this.authenticated = false;
|
|
3311
2137
|
this.joined = false;
|
|
3312
|
-
this._mediaMembers = [];
|
|
3313
2138
|
}
|
|
3314
2139
|
handleAuthenticationFailure(error) {
|
|
3315
2140
|
const authError = error instanceof core.EdgeBaseError ? error : new core.EdgeBaseError(500, "Room authentication failed.");
|
|
@@ -3328,17 +2153,35 @@ var RoomClient = class _RoomClient {
|
|
|
3328
2153
|
}
|
|
3329
2154
|
}
|
|
3330
2155
|
}
|
|
3331
|
-
|
|
3332
|
-
if (
|
|
3333
|
-
return
|
|
2156
|
+
shouldTreatErrorAsAuthLoss(code) {
|
|
2157
|
+
if (code === "AUTH_STATE_LOST") {
|
|
2158
|
+
return true;
|
|
3334
2159
|
}
|
|
3335
|
-
|
|
2160
|
+
if (code !== "NOT_AUTHENTICATED") {
|
|
2161
|
+
return false;
|
|
2162
|
+
}
|
|
2163
|
+
return this.authenticated || this.hasPendingRequests();
|
|
2164
|
+
}
|
|
2165
|
+
hasPendingRequests() {
|
|
2166
|
+
return this.pendingRequests.size > 0 || this.pendingSignalRequests.size > 0 || this.pendingAdminRequests.size > 0 || this.pendingMemberStateRequests.size > 0;
|
|
3336
2167
|
}
|
|
3337
|
-
|
|
2168
|
+
handleRoomAuthStateLoss(message) {
|
|
2169
|
+
const detail = message?.trim();
|
|
2170
|
+
const authLossMessage = detail ? `Room authentication lost: ${detail}` : "Room authentication lost";
|
|
2171
|
+
this.setConnectionState("auth_lost");
|
|
2172
|
+
this.rejectAllPendingRequests(new core.EdgeBaseError(401, authLossMessage));
|
|
2173
|
+
if (this.ws && this.ws.readyState === WS_OPEN) {
|
|
2174
|
+
try {
|
|
2175
|
+
this.ws.close(ROOM_AUTH_STATE_LOST_CLOSE_CODE, authLossMessage);
|
|
2176
|
+
} catch {
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
normalizeMembers(value) {
|
|
3338
2181
|
if (!Array.isArray(value)) {
|
|
3339
2182
|
return [];
|
|
3340
2183
|
}
|
|
3341
|
-
return value.map((member) => this.
|
|
2184
|
+
return value.map((member) => this.normalizeMember(member)).filter((member) => !!member);
|
|
3342
2185
|
}
|
|
3343
2186
|
normalizeMember(value) {
|
|
3344
2187
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
@@ -3363,80 +2206,6 @@ var RoomClient = class _RoomClient {
|
|
|
3363
2206
|
}
|
|
3364
2207
|
return cloneRecord(value);
|
|
3365
2208
|
}
|
|
3366
|
-
normalizeMediaMember(value) {
|
|
3367
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
3368
|
-
return null;
|
|
3369
|
-
}
|
|
3370
|
-
const entry = value;
|
|
3371
|
-
const member = this.normalizeMember(entry.member);
|
|
3372
|
-
if (!member) {
|
|
3373
|
-
return null;
|
|
3374
|
-
}
|
|
3375
|
-
return {
|
|
3376
|
-
member,
|
|
3377
|
-
state: this.normalizeMediaState(entry.state),
|
|
3378
|
-
tracks: this.normalizeMediaTracks(entry.tracks)
|
|
3379
|
-
};
|
|
3380
|
-
}
|
|
3381
|
-
normalizeMediaState(value) {
|
|
3382
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
3383
|
-
return {};
|
|
3384
|
-
}
|
|
3385
|
-
const state = value;
|
|
3386
|
-
return {
|
|
3387
|
-
audio: this.normalizeMediaKindState(state.audio),
|
|
3388
|
-
video: this.normalizeMediaKindState(state.video),
|
|
3389
|
-
screen: this.normalizeMediaKindState(state.screen)
|
|
3390
|
-
};
|
|
3391
|
-
}
|
|
3392
|
-
normalizeMediaKindState(value) {
|
|
3393
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
3394
|
-
return void 0;
|
|
3395
|
-
}
|
|
3396
|
-
const state = value;
|
|
3397
|
-
return {
|
|
3398
|
-
published: state.published === true,
|
|
3399
|
-
muted: state.muted === true,
|
|
3400
|
-
trackId: typeof state.trackId === "string" ? state.trackId : void 0,
|
|
3401
|
-
deviceId: typeof state.deviceId === "string" ? state.deviceId : void 0,
|
|
3402
|
-
publishedAt: typeof state.publishedAt === "number" ? state.publishedAt : void 0,
|
|
3403
|
-
adminDisabled: state.adminDisabled === true
|
|
3404
|
-
};
|
|
3405
|
-
}
|
|
3406
|
-
normalizeMediaTracks(value) {
|
|
3407
|
-
if (!Array.isArray(value)) {
|
|
3408
|
-
return [];
|
|
3409
|
-
}
|
|
3410
|
-
return value.map((track) => this.normalizeMediaTrack(track)).filter((track) => !!track);
|
|
3411
|
-
}
|
|
3412
|
-
normalizeMediaTrack(value) {
|
|
3413
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
3414
|
-
return null;
|
|
3415
|
-
}
|
|
3416
|
-
const track = value;
|
|
3417
|
-
const kind = this.normalizeMediaKind(track.kind);
|
|
3418
|
-
if (!kind) {
|
|
3419
|
-
return null;
|
|
3420
|
-
}
|
|
3421
|
-
return {
|
|
3422
|
-
kind,
|
|
3423
|
-
trackId: typeof track.trackId === "string" ? track.trackId : void 0,
|
|
3424
|
-
deviceId: typeof track.deviceId === "string" ? track.deviceId : void 0,
|
|
3425
|
-
muted: track.muted === true,
|
|
3426
|
-
publishedAt: typeof track.publishedAt === "number" ? track.publishedAt : void 0,
|
|
3427
|
-
adminDisabled: track.adminDisabled === true
|
|
3428
|
-
};
|
|
3429
|
-
}
|
|
3430
|
-
normalizeMediaKind(value) {
|
|
3431
|
-
switch (value) {
|
|
3432
|
-
case "audio":
|
|
3433
|
-
case "video":
|
|
3434
|
-
case "screen":
|
|
3435
|
-
return value;
|
|
3436
|
-
default:
|
|
3437
|
-
return null;
|
|
3438
|
-
}
|
|
3439
|
-
}
|
|
3440
2209
|
normalizeSignalMeta(value) {
|
|
3441
2210
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
3442
2211
|
return {};
|
|
@@ -3460,6 +2229,20 @@ var RoomClient = class _RoomClient {
|
|
|
3460
2229
|
return "leave";
|
|
3461
2230
|
}
|
|
3462
2231
|
}
|
|
2232
|
+
getDebugSnapshot() {
|
|
2233
|
+
return {
|
|
2234
|
+
connectionState: this.connectionState,
|
|
2235
|
+
connected: this.connected,
|
|
2236
|
+
authenticated: this.authenticated,
|
|
2237
|
+
joined: this.joined,
|
|
2238
|
+
currentUserId: this.currentUserId,
|
|
2239
|
+
currentConnectionId: this.currentConnectionId,
|
|
2240
|
+
membersCount: this._members.length,
|
|
2241
|
+
reconnectAttempts: this.reconnectAttempts,
|
|
2242
|
+
joinRequested: this.joinRequested,
|
|
2243
|
+
waitingForAuth: this.waitingForAuth
|
|
2244
|
+
};
|
|
2245
|
+
}
|
|
3463
2246
|
upsertMember(member) {
|
|
3464
2247
|
const index = this._members.findIndex((entry) => entry.memberId === member.memberId);
|
|
3465
2248
|
if (index >= 0) {
|
|
@@ -3471,62 +2254,7 @@ var RoomClient = class _RoomClient {
|
|
|
3471
2254
|
removeMember(memberId) {
|
|
3472
2255
|
this._members = this._members.filter((member) => member.memberId !== memberId);
|
|
3473
2256
|
}
|
|
3474
|
-
|
|
3475
|
-
const mediaMember = this._mediaMembers.find((entry) => entry.member.memberId === member.memberId);
|
|
3476
|
-
if (!mediaMember) {
|
|
3477
|
-
return;
|
|
3478
|
-
}
|
|
3479
|
-
mediaMember.member = cloneValue(member);
|
|
3480
|
-
}
|
|
3481
|
-
ensureMediaMember(member) {
|
|
3482
|
-
const existing = this._mediaMembers.find((entry) => entry.member.memberId === member.memberId);
|
|
3483
|
-
if (existing) {
|
|
3484
|
-
existing.member = cloneValue(member);
|
|
3485
|
-
return existing;
|
|
3486
|
-
}
|
|
3487
|
-
const created = {
|
|
3488
|
-
member: cloneValue(member),
|
|
3489
|
-
state: {},
|
|
3490
|
-
tracks: []
|
|
3491
|
-
};
|
|
3492
|
-
this._mediaMembers.push(created);
|
|
3493
|
-
return created;
|
|
3494
|
-
}
|
|
3495
|
-
removeMediaMember(memberId) {
|
|
3496
|
-
this._mediaMembers = this._mediaMembers.filter((member) => member.member.memberId !== memberId);
|
|
3497
|
-
}
|
|
3498
|
-
upsertMediaTrack(mediaMember, track) {
|
|
3499
|
-
const index = mediaMember.tracks.findIndex(
|
|
3500
|
-
(entry) => entry.kind === track.kind && entry.trackId === track.trackId
|
|
3501
|
-
);
|
|
3502
|
-
if (index >= 0) {
|
|
3503
|
-
mediaMember.tracks[index] = cloneValue(track);
|
|
3504
|
-
return;
|
|
3505
|
-
}
|
|
3506
|
-
mediaMember.tracks = mediaMember.tracks.filter((entry) => !(entry.kind === track.kind && !track.trackId)).concat(cloneValue(track));
|
|
3507
|
-
}
|
|
3508
|
-
removeMediaTrack(mediaMember, track) {
|
|
3509
|
-
mediaMember.tracks = mediaMember.tracks.filter((entry) => {
|
|
3510
|
-
if (track.trackId) {
|
|
3511
|
-
return !(entry.kind === track.kind && entry.trackId === track.trackId);
|
|
3512
|
-
}
|
|
3513
|
-
return entry.kind !== track.kind;
|
|
3514
|
-
});
|
|
3515
|
-
}
|
|
3516
|
-
mergeMediaState(mediaMember, kind, partial) {
|
|
3517
|
-
const next = {
|
|
3518
|
-
published: partial.published ?? mediaMember.state[kind]?.published ?? false,
|
|
3519
|
-
muted: partial.muted ?? mediaMember.state[kind]?.muted ?? false,
|
|
3520
|
-
trackId: partial.trackId ?? mediaMember.state[kind]?.trackId,
|
|
3521
|
-
deviceId: partial.deviceId ?? mediaMember.state[kind]?.deviceId,
|
|
3522
|
-
publishedAt: partial.publishedAt ?? mediaMember.state[kind]?.publishedAt,
|
|
3523
|
-
adminDisabled: partial.adminDisabled ?? mediaMember.state[kind]?.adminDisabled
|
|
3524
|
-
};
|
|
3525
|
-
mediaMember.state = {
|
|
3526
|
-
...mediaMember.state,
|
|
3527
|
-
[kind]: next
|
|
3528
|
-
};
|
|
3529
|
-
}
|
|
2257
|
+
/** Reject all 5 pending request maps at once. */
|
|
3530
2258
|
rejectAllPendingRequests(error) {
|
|
3531
2259
|
for (const [, pending] of this.pendingRequests) {
|
|
3532
2260
|
clearTimeout(pending.timeout);
|
|
@@ -3536,7 +2264,6 @@ var RoomClient = class _RoomClient {
|
|
|
3536
2264
|
this.rejectPendingVoidRequests(this.pendingSignalRequests, error);
|
|
3537
2265
|
this.rejectPendingVoidRequests(this.pendingAdminRequests, error);
|
|
3538
2266
|
this.rejectPendingVoidRequests(this.pendingMemberStateRequests, error);
|
|
3539
|
-
this.rejectPendingVoidRequests(this.pendingMediaRequests, error);
|
|
3540
2267
|
}
|
|
3541
2268
|
rejectPendingVoidRequests(pendingRequests, error) {
|
|
3542
2269
|
for (const [, pending] of pendingRequests) {
|
|
@@ -3545,11 +2272,56 @@ var RoomClient = class _RoomClient {
|
|
|
3545
2272
|
}
|
|
3546
2273
|
pendingRequests.clear();
|
|
3547
2274
|
}
|
|
2275
|
+
shouldScheduleDisconnectReset(next) {
|
|
2276
|
+
if (this.intentionallyLeft || !this.joinRequested) {
|
|
2277
|
+
return false;
|
|
2278
|
+
}
|
|
2279
|
+
return next === "disconnected";
|
|
2280
|
+
}
|
|
2281
|
+
clearDisconnectResetTimer() {
|
|
2282
|
+
if (this.disconnectResetTimer) {
|
|
2283
|
+
clearTimeout(this.disconnectResetTimer);
|
|
2284
|
+
this.disconnectResetTimer = null;
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
scheduleDisconnectReset(stateAtSchedule) {
|
|
2288
|
+
this.clearDisconnectResetTimer();
|
|
2289
|
+
const timeoutMs = this.options.disconnectResetTimeoutMs;
|
|
2290
|
+
if (!(timeoutMs > 0)) {
|
|
2291
|
+
return;
|
|
2292
|
+
}
|
|
2293
|
+
this.disconnectResetTimer = setTimeout(() => {
|
|
2294
|
+
this.disconnectResetTimer = null;
|
|
2295
|
+
if (this.intentionallyLeft || !this.joinRequested) {
|
|
2296
|
+
return;
|
|
2297
|
+
}
|
|
2298
|
+
if (this.connectionState !== stateAtSchedule) {
|
|
2299
|
+
return;
|
|
2300
|
+
}
|
|
2301
|
+
if (this.connectionState === "connected") {
|
|
2302
|
+
return;
|
|
2303
|
+
}
|
|
2304
|
+
for (const handler of this.recoveryFailureHandlers) {
|
|
2305
|
+
try {
|
|
2306
|
+
handler({
|
|
2307
|
+
state: this.connectionState,
|
|
2308
|
+
timeoutMs
|
|
2309
|
+
});
|
|
2310
|
+
} catch {
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
}, timeoutMs);
|
|
2314
|
+
}
|
|
3548
2315
|
setConnectionState(next) {
|
|
3549
2316
|
if (this.connectionState === next) {
|
|
3550
2317
|
return;
|
|
3551
2318
|
}
|
|
3552
2319
|
this.connectionState = next;
|
|
2320
|
+
if (this.shouldScheduleDisconnectReset(next)) {
|
|
2321
|
+
this.scheduleDisconnectReset(next);
|
|
2322
|
+
} else {
|
|
2323
|
+
this.clearDisconnectResetTimer();
|
|
2324
|
+
}
|
|
3553
2325
|
for (const handler of this.connectionStateHandlers) {
|
|
3554
2326
|
handler(next);
|
|
3555
2327
|
}
|
|
@@ -3566,6 +2338,30 @@ var RoomClient = class _RoomClient {
|
|
|
3566
2338
|
const wsUrl = httpUrl.replace(/^http/, "ws");
|
|
3567
2339
|
return `${wsUrl}/api/room?namespace=${encodeURIComponent(this.namespace)}&id=${encodeURIComponent(this.roomId)}`;
|
|
3568
2340
|
}
|
|
2341
|
+
attachBrowserNetworkListeners() {
|
|
2342
|
+
if (this.browserNetworkListenersAttached) {
|
|
2343
|
+
return;
|
|
2344
|
+
}
|
|
2345
|
+
const eventTarget = typeof globalThis !== "undefined" && typeof globalThis.addEventListener === "function" ? globalThis : null;
|
|
2346
|
+
if (!eventTarget) {
|
|
2347
|
+
return;
|
|
2348
|
+
}
|
|
2349
|
+
eventTarget.addEventListener("offline", this.browserOfflineHandler);
|
|
2350
|
+
eventTarget.addEventListener("online", this.browserOnlineHandler);
|
|
2351
|
+
this.browserNetworkListenersAttached = true;
|
|
2352
|
+
}
|
|
2353
|
+
detachBrowserNetworkListeners() {
|
|
2354
|
+
if (!this.browserNetworkListenersAttached) {
|
|
2355
|
+
return;
|
|
2356
|
+
}
|
|
2357
|
+
const eventTarget = typeof globalThis !== "undefined" && typeof globalThis.removeEventListener === "function" ? globalThis : null;
|
|
2358
|
+
if (!eventTarget) {
|
|
2359
|
+
return;
|
|
2360
|
+
}
|
|
2361
|
+
eventTarget.removeEventListener("offline", this.browserOfflineHandler);
|
|
2362
|
+
eventTarget.removeEventListener("online", this.browserOnlineHandler);
|
|
2363
|
+
this.browserNetworkListenersAttached = false;
|
|
2364
|
+
}
|
|
3569
2365
|
scheduleReconnect() {
|
|
3570
2366
|
const attempt = this.reconnectAttempts + 1;
|
|
3571
2367
|
const baseDelay = this.options.reconnectBaseDelay * Math.pow(2, this.reconnectAttempts);
|
|
@@ -3584,11 +2380,19 @@ var RoomClient = class _RoomClient {
|
|
|
3584
2380
|
}
|
|
3585
2381
|
startHeartbeat() {
|
|
3586
2382
|
this.stopHeartbeat();
|
|
2383
|
+
this.lastHeartbeatAckAt = Date.now();
|
|
3587
2384
|
this.heartbeatTimer = setInterval(() => {
|
|
3588
2385
|
if (this.ws && this.connected) {
|
|
2386
|
+
if (Date.now() - this.lastHeartbeatAckAt > this.options.heartbeatStaleTimeoutMs) {
|
|
2387
|
+
try {
|
|
2388
|
+
this.ws.close();
|
|
2389
|
+
} catch {
|
|
2390
|
+
}
|
|
2391
|
+
return;
|
|
2392
|
+
}
|
|
3589
2393
|
this.ws.send(JSON.stringify({ type: "ping" }));
|
|
3590
2394
|
}
|
|
3591
|
-
},
|
|
2395
|
+
}, this.options.heartbeatIntervalMs);
|
|
3592
2396
|
}
|
|
3593
2397
|
stopHeartbeat() {
|
|
3594
2398
|
if (this.heartbeatTimer) {
|
|
@@ -3596,6 +2400,12 @@ var RoomClient = class _RoomClient {
|
|
|
3596
2400
|
this.heartbeatTimer = null;
|
|
3597
2401
|
}
|
|
3598
2402
|
}
|
|
2403
|
+
getReconnectMemberState() {
|
|
2404
|
+
if (!this.lastLocalMemberState) {
|
|
2405
|
+
return void 0;
|
|
2406
|
+
}
|
|
2407
|
+
return cloneRecord(this.lastLocalMemberState);
|
|
2408
|
+
}
|
|
3599
2409
|
};
|
|
3600
2410
|
var PushClient = class {
|
|
3601
2411
|
constructor(http, storage, core) {
|