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