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