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