@edge-base/react-native 0.1.5 → 0.2.1
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 +51 -0
- package/dist/index.cjs +1326 -131
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +178 -11
- package/dist/index.d.ts +178 -11
- package/dist/index.js +1327 -132
- package/dist/index.js.map +1 -1
- package/package.json +12 -4
package/dist/index.cjs
CHANGED
|
@@ -652,7 +652,7 @@ var DatabaseLiveClient = class {
|
|
|
652
652
|
}
|
|
653
653
|
this.connect(channel).catch(() => {
|
|
654
654
|
});
|
|
655
|
-
return () => {
|
|
655
|
+
return core.createSubscription(() => {
|
|
656
656
|
const subs = this.subscriptions.get(channel);
|
|
657
657
|
if (!subs) return;
|
|
658
658
|
const idx = subs.indexOf(sub);
|
|
@@ -663,14 +663,14 @@ var DatabaseLiveClient = class {
|
|
|
663
663
|
this.channelOrFilters.delete(channel);
|
|
664
664
|
this.sendUnsubscribe(channel);
|
|
665
665
|
}
|
|
666
|
-
};
|
|
666
|
+
});
|
|
667
667
|
}
|
|
668
668
|
onError(handler) {
|
|
669
669
|
this.errorHandlers.push(handler);
|
|
670
|
-
return () => {
|
|
670
|
+
return core.createSubscription(() => {
|
|
671
671
|
const idx = this.errorHandlers.indexOf(handler);
|
|
672
672
|
if (idx >= 0) this.errorHandlers.splice(idx, 1);
|
|
673
|
-
};
|
|
673
|
+
});
|
|
674
674
|
}
|
|
675
675
|
async connect(channel) {
|
|
676
676
|
this.connectedChannels.add(channel);
|
|
@@ -758,7 +758,7 @@ var DatabaseLiveClient = class {
|
|
|
758
758
|
(refreshToken) => refreshAccessToken(this.baseUrl, refreshToken)
|
|
759
759
|
);
|
|
760
760
|
if (!token) throw new core.EdgeBaseError(401, "No access token available. Sign in first.");
|
|
761
|
-
this.sendRaw({ type: "auth", token, sdkVersion: "0.1
|
|
761
|
+
this.sendRaw({ type: "auth", token, sdkVersion: "0.2.1" });
|
|
762
762
|
return new Promise((resolve, reject) => {
|
|
763
763
|
const timeout = setTimeout(() => reject(new core.EdgeBaseError(401, "Auth timeout")), 1e4);
|
|
764
764
|
const original = this.ws?.onmessage;
|
|
@@ -878,7 +878,7 @@ var DatabaseLiveClient = class {
|
|
|
878
878
|
refreshAuth() {
|
|
879
879
|
const token = this.tokenManager.currentAccessToken;
|
|
880
880
|
if (!token || !this.ws || !this.connected) return;
|
|
881
|
-
this.sendRaw({ type: "auth", token, sdkVersion: "0.1
|
|
881
|
+
this.sendRaw({ type: "auth", token, sdkVersion: "0.2.1" });
|
|
882
882
|
}
|
|
883
883
|
handleAuthStateChange(user) {
|
|
884
884
|
if (user) {
|
|
@@ -941,7 +941,9 @@ var DatabaseLiveClient = class {
|
|
|
941
941
|
}
|
|
942
942
|
}
|
|
943
943
|
scheduleReconnect(channel) {
|
|
944
|
-
const
|
|
944
|
+
const baseDelay = this.options.reconnectBaseDelay * Math.pow(2, this.reconnectAttempts);
|
|
945
|
+
const jitter = Math.random() * baseDelay * 0.25;
|
|
946
|
+
const delay = baseDelay + jitter;
|
|
945
947
|
this.reconnectAttempts++;
|
|
946
948
|
setTimeout(() => {
|
|
947
949
|
this.connect(channel).catch(() => {
|
|
@@ -1017,11 +1019,1115 @@ function matchesDatabaseLiveChannel(channel, change, messageChannel) {
|
|
|
1017
1019
|
if (parts.length === 2) return parts[1] === change.table;
|
|
1018
1020
|
if (parts.length === 3) return parts[2] === change.table;
|
|
1019
1021
|
if (parts.length === 4) {
|
|
1020
|
-
if (parts[2] === change.table
|
|
1022
|
+
if (parts[2] === change.table && change.docId === parts[3]) return true;
|
|
1021
1023
|
return parts[3] === change.table;
|
|
1022
1024
|
}
|
|
1023
1025
|
return parts[3] === change.table && change.docId === parts[4];
|
|
1024
1026
|
}
|
|
1027
|
+
|
|
1028
|
+
// src/room-cloudflare-media.ts
|
|
1029
|
+
function buildRemoteTrackKey(participantId, kind) {
|
|
1030
|
+
return `${participantId}:${kind}`;
|
|
1031
|
+
}
|
|
1032
|
+
function installMessage(packageName) {
|
|
1033
|
+
return `Install ${packageName} to use React Native room media transport. See https://edgebase.fun/docs/room/media`;
|
|
1034
|
+
}
|
|
1035
|
+
var RoomCloudflareMediaTransport = class {
|
|
1036
|
+
room;
|
|
1037
|
+
options;
|
|
1038
|
+
localTracks = /* @__PURE__ */ new Map();
|
|
1039
|
+
remoteTrackHandlers = [];
|
|
1040
|
+
participantListeners = /* @__PURE__ */ new Map();
|
|
1041
|
+
remoteTrackIds = /* @__PURE__ */ new Map();
|
|
1042
|
+
clientFactoryPromise = null;
|
|
1043
|
+
connectPromise = null;
|
|
1044
|
+
lifecycleVersion = 0;
|
|
1045
|
+
meeting = null;
|
|
1046
|
+
sessionId = null;
|
|
1047
|
+
joinedMapSubscriptionsAttached = false;
|
|
1048
|
+
onParticipantJoined = (participant) => {
|
|
1049
|
+
this.attachParticipant(participant);
|
|
1050
|
+
};
|
|
1051
|
+
onParticipantLeft = (participant) => {
|
|
1052
|
+
this.detachParticipant(participant.id);
|
|
1053
|
+
};
|
|
1054
|
+
onParticipantsCleared = () => {
|
|
1055
|
+
this.clearParticipantListeners();
|
|
1056
|
+
};
|
|
1057
|
+
onParticipantsUpdate = () => {
|
|
1058
|
+
this.syncAllParticipants();
|
|
1059
|
+
};
|
|
1060
|
+
constructor(room, options) {
|
|
1061
|
+
this.room = room;
|
|
1062
|
+
this.options = {
|
|
1063
|
+
autoSubscribe: options?.autoSubscribe ?? true,
|
|
1064
|
+
mediaDevices: options?.mediaDevices ?? (typeof navigator !== "undefined" ? navigator.mediaDevices : void 0),
|
|
1065
|
+
clientFactory: options?.clientFactory
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
getSessionId() {
|
|
1069
|
+
return this.sessionId;
|
|
1070
|
+
}
|
|
1071
|
+
getPeerConnection() {
|
|
1072
|
+
return null;
|
|
1073
|
+
}
|
|
1074
|
+
async connect(payload) {
|
|
1075
|
+
if (this.meeting && this.sessionId) {
|
|
1076
|
+
return this.sessionId;
|
|
1077
|
+
}
|
|
1078
|
+
if (this.connectPromise) {
|
|
1079
|
+
return this.connectPromise;
|
|
1080
|
+
}
|
|
1081
|
+
const connectPromise = (async () => {
|
|
1082
|
+
const lifecycleVersion = this.lifecycleVersion;
|
|
1083
|
+
const session = await this.room.media.cloudflareRealtimeKit.createSession(payload);
|
|
1084
|
+
this.assertConnectStillActive(lifecycleVersion);
|
|
1085
|
+
const factory = await this.resolveClientFactory();
|
|
1086
|
+
this.assertConnectStillActive(lifecycleVersion);
|
|
1087
|
+
const meeting = await factory.init({
|
|
1088
|
+
authToken: session.authToken,
|
|
1089
|
+
defaults: {
|
|
1090
|
+
audio: false,
|
|
1091
|
+
video: false
|
|
1092
|
+
}
|
|
1093
|
+
});
|
|
1094
|
+
this.assertConnectStillActive(lifecycleVersion, meeting);
|
|
1095
|
+
this.meeting = meeting;
|
|
1096
|
+
this.sessionId = session.sessionId;
|
|
1097
|
+
this.attachParticipantMapListeners();
|
|
1098
|
+
try {
|
|
1099
|
+
if (meeting.join) {
|
|
1100
|
+
await meeting.join();
|
|
1101
|
+
} else if (meeting.joinRoom) {
|
|
1102
|
+
await meeting.joinRoom();
|
|
1103
|
+
} else {
|
|
1104
|
+
throw new Error("RealtimeKit client does not expose join()/joinRoom().");
|
|
1105
|
+
}
|
|
1106
|
+
this.assertConnectStillActive(lifecycleVersion, meeting);
|
|
1107
|
+
this.syncAllParticipants();
|
|
1108
|
+
return session.sessionId;
|
|
1109
|
+
} catch (error) {
|
|
1110
|
+
if (this.meeting === meeting) {
|
|
1111
|
+
this.cleanupMeeting();
|
|
1112
|
+
} else {
|
|
1113
|
+
this.leaveMeetingSilently(meeting);
|
|
1114
|
+
}
|
|
1115
|
+
throw error;
|
|
1116
|
+
}
|
|
1117
|
+
})();
|
|
1118
|
+
this.connectPromise = connectPromise;
|
|
1119
|
+
try {
|
|
1120
|
+
return await connectPromise;
|
|
1121
|
+
} finally {
|
|
1122
|
+
if (this.connectPromise === connectPromise) {
|
|
1123
|
+
this.connectPromise = null;
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
async enableAudio(constraints = true) {
|
|
1128
|
+
const meeting = await this.ensureMeeting();
|
|
1129
|
+
const customTrack = await this.createUserMediaTrack("audio", constraints);
|
|
1130
|
+
await meeting.self.enableAudio(customTrack ?? void 0);
|
|
1131
|
+
const track = meeting.self.audioTrack ?? customTrack;
|
|
1132
|
+
if (!track) {
|
|
1133
|
+
throw new Error("RealtimeKit did not expose a local audio track after enabling audio.");
|
|
1134
|
+
}
|
|
1135
|
+
this.rememberLocalTrack("audio", track, track.getSettings().deviceId ?? customTrack?.getSettings().deviceId, !!customTrack);
|
|
1136
|
+
await this.room.media.audio.enable?.({
|
|
1137
|
+
trackId: track.id,
|
|
1138
|
+
deviceId: this.localTracks.get("audio")?.deviceId,
|
|
1139
|
+
providerSessionId: meeting.self.id
|
|
1140
|
+
});
|
|
1141
|
+
return track;
|
|
1142
|
+
}
|
|
1143
|
+
async enableVideo(constraints = true) {
|
|
1144
|
+
const meeting = await this.ensureMeeting();
|
|
1145
|
+
const customTrack = await this.createUserMediaTrack("video", constraints);
|
|
1146
|
+
await meeting.self.enableVideo(customTrack ?? void 0);
|
|
1147
|
+
const track = meeting.self.videoTrack ?? customTrack;
|
|
1148
|
+
if (!track) {
|
|
1149
|
+
throw new Error("RealtimeKit did not expose a local video track after enabling video.");
|
|
1150
|
+
}
|
|
1151
|
+
this.rememberLocalTrack("video", track, track.getSettings().deviceId ?? customTrack?.getSettings().deviceId, !!customTrack);
|
|
1152
|
+
await this.room.media.video.enable?.({
|
|
1153
|
+
trackId: track.id,
|
|
1154
|
+
deviceId: this.localTracks.get("video")?.deviceId,
|
|
1155
|
+
providerSessionId: meeting.self.id
|
|
1156
|
+
});
|
|
1157
|
+
return track;
|
|
1158
|
+
}
|
|
1159
|
+
async startScreenShare(_constraints = { video: true, audio: false }) {
|
|
1160
|
+
const meeting = await this.ensureMeeting();
|
|
1161
|
+
await meeting.self.enableScreenShare();
|
|
1162
|
+
const track = meeting.self.screenShareTracks?.video;
|
|
1163
|
+
if (!track) {
|
|
1164
|
+
throw new Error("RealtimeKit did not expose a screen-share video track.");
|
|
1165
|
+
}
|
|
1166
|
+
track.addEventListener("ended", () => {
|
|
1167
|
+
void this.stopScreenShare();
|
|
1168
|
+
}, { once: true });
|
|
1169
|
+
this.rememberLocalTrack("screen", track, track.getSettings().deviceId, false);
|
|
1170
|
+
await this.room.media.screen.start?.({
|
|
1171
|
+
trackId: track.id,
|
|
1172
|
+
deviceId: track.getSettings().deviceId,
|
|
1173
|
+
providerSessionId: meeting.self.id
|
|
1174
|
+
});
|
|
1175
|
+
return track;
|
|
1176
|
+
}
|
|
1177
|
+
async disableAudio() {
|
|
1178
|
+
if (!this.meeting) return;
|
|
1179
|
+
await this.meeting.self.disableAudio();
|
|
1180
|
+
this.releaseLocalTrack("audio");
|
|
1181
|
+
await this.room.media.audio.disable();
|
|
1182
|
+
}
|
|
1183
|
+
async disableVideo() {
|
|
1184
|
+
if (!this.meeting) return;
|
|
1185
|
+
await this.meeting.self.disableVideo();
|
|
1186
|
+
this.releaseLocalTrack("video");
|
|
1187
|
+
await this.room.media.video.disable();
|
|
1188
|
+
}
|
|
1189
|
+
async stopScreenShare() {
|
|
1190
|
+
if (!this.meeting) return;
|
|
1191
|
+
await this.meeting.self.disableScreenShare();
|
|
1192
|
+
this.releaseLocalTrack("screen");
|
|
1193
|
+
await this.room.media.screen.stop();
|
|
1194
|
+
}
|
|
1195
|
+
async setMuted(kind, muted) {
|
|
1196
|
+
const localTrack = this.localTracks.get(kind)?.track ?? (kind === "audio" ? this.meeting?.self.audioTrack : this.meeting?.self.videoTrack);
|
|
1197
|
+
if (localTrack) {
|
|
1198
|
+
localTrack.enabled = !muted;
|
|
1199
|
+
}
|
|
1200
|
+
if (kind === "audio") {
|
|
1201
|
+
await this.room.media.audio.setMuted?.(muted);
|
|
1202
|
+
} else {
|
|
1203
|
+
await this.room.media.video.setMuted?.(muted);
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
async switchDevices(payload) {
|
|
1207
|
+
const meeting = await this.ensureMeeting();
|
|
1208
|
+
if (payload.audioInputId) {
|
|
1209
|
+
const audioDevice = await meeting.self.getDeviceById(payload.audioInputId, "audio");
|
|
1210
|
+
await meeting.self.setDevice(audioDevice);
|
|
1211
|
+
const audioTrack = meeting.self.audioTrack;
|
|
1212
|
+
if (audioTrack) {
|
|
1213
|
+
this.rememberLocalTrack("audio", audioTrack, payload.audioInputId, false);
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
if (payload.videoInputId) {
|
|
1217
|
+
const videoDevice = await meeting.self.getDeviceById(payload.videoInputId, "video");
|
|
1218
|
+
await meeting.self.setDevice(videoDevice);
|
|
1219
|
+
const videoTrack = meeting.self.videoTrack;
|
|
1220
|
+
if (videoTrack) {
|
|
1221
|
+
this.rememberLocalTrack("video", videoTrack, payload.videoInputId, false);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
await this.room.media.devices.switch(payload);
|
|
1225
|
+
}
|
|
1226
|
+
onRemoteTrack(handler) {
|
|
1227
|
+
this.remoteTrackHandlers.push(handler);
|
|
1228
|
+
return core.createSubscription(() => {
|
|
1229
|
+
const index = this.remoteTrackHandlers.indexOf(handler);
|
|
1230
|
+
if (index >= 0) {
|
|
1231
|
+
this.remoteTrackHandlers.splice(index, 1);
|
|
1232
|
+
}
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
destroy() {
|
|
1236
|
+
this.lifecycleVersion += 1;
|
|
1237
|
+
this.connectPromise = null;
|
|
1238
|
+
for (const kind of this.localTracks.keys()) {
|
|
1239
|
+
this.releaseLocalTrack(kind);
|
|
1240
|
+
}
|
|
1241
|
+
this.clearParticipantListeners();
|
|
1242
|
+
this.detachParticipantMapListeners();
|
|
1243
|
+
this.cleanupMeeting();
|
|
1244
|
+
}
|
|
1245
|
+
async ensureMeeting() {
|
|
1246
|
+
if (!this.meeting) {
|
|
1247
|
+
await this.connect();
|
|
1248
|
+
}
|
|
1249
|
+
if (!this.meeting) {
|
|
1250
|
+
throw new Error("Cloudflare media transport is not connected");
|
|
1251
|
+
}
|
|
1252
|
+
return this.meeting;
|
|
1253
|
+
}
|
|
1254
|
+
async resolveClientFactory() {
|
|
1255
|
+
if (this.options.clientFactory) {
|
|
1256
|
+
return this.options.clientFactory;
|
|
1257
|
+
}
|
|
1258
|
+
this.clientFactoryPromise ??= import('@cloudflare/realtimekit-react-native').then((mod) => mod.default ?? mod).catch((error) => {
|
|
1259
|
+
throw new Error(`${installMessage("@cloudflare/realtimekit-react-native")}
|
|
1260
|
+
${String(error)}`);
|
|
1261
|
+
});
|
|
1262
|
+
return this.clientFactoryPromise;
|
|
1263
|
+
}
|
|
1264
|
+
async createUserMediaTrack(kind, constraints) {
|
|
1265
|
+
const devices = this.options.mediaDevices;
|
|
1266
|
+
if (!devices?.getUserMedia || constraints === false) {
|
|
1267
|
+
return null;
|
|
1268
|
+
}
|
|
1269
|
+
const stream = await devices.getUserMedia(
|
|
1270
|
+
kind === "audio" ? { audio: constraints, video: false } : { audio: false, video: constraints }
|
|
1271
|
+
);
|
|
1272
|
+
return kind === "audio" ? stream.getAudioTracks()[0] ?? null : stream.getVideoTracks()[0] ?? null;
|
|
1273
|
+
}
|
|
1274
|
+
rememberLocalTrack(kind, track, deviceId, stopOnCleanup) {
|
|
1275
|
+
this.releaseLocalTrack(kind);
|
|
1276
|
+
this.localTracks.set(kind, {
|
|
1277
|
+
kind,
|
|
1278
|
+
track,
|
|
1279
|
+
deviceId,
|
|
1280
|
+
stopOnCleanup
|
|
1281
|
+
});
|
|
1282
|
+
}
|
|
1283
|
+
releaseLocalTrack(kind) {
|
|
1284
|
+
const local = this.localTracks.get(kind);
|
|
1285
|
+
if (!local) return;
|
|
1286
|
+
if (local.stopOnCleanup) {
|
|
1287
|
+
local.track.stop();
|
|
1288
|
+
}
|
|
1289
|
+
this.localTracks.delete(kind);
|
|
1290
|
+
}
|
|
1291
|
+
attachParticipantMapListeners() {
|
|
1292
|
+
const participantMap = this.getParticipantMap();
|
|
1293
|
+
if (!participantMap || !this.meeting || this.joinedMapSubscriptionsAttached) {
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
participantMap.on("participantJoined", this.onParticipantJoined);
|
|
1297
|
+
participantMap.on("participantLeft", this.onParticipantLeft);
|
|
1298
|
+
participantMap.on("participantsCleared", this.onParticipantsCleared);
|
|
1299
|
+
participantMap.on("participantsUpdate", this.onParticipantsUpdate);
|
|
1300
|
+
this.joinedMapSubscriptionsAttached = true;
|
|
1301
|
+
}
|
|
1302
|
+
detachParticipantMapListeners() {
|
|
1303
|
+
const participantMap = this.getParticipantMap();
|
|
1304
|
+
if (!participantMap || !this.meeting || !this.joinedMapSubscriptionsAttached) {
|
|
1305
|
+
return;
|
|
1306
|
+
}
|
|
1307
|
+
participantMap.off("participantJoined", this.onParticipantJoined);
|
|
1308
|
+
participantMap.off("participantLeft", this.onParticipantLeft);
|
|
1309
|
+
participantMap.off("participantsCleared", this.onParticipantsCleared);
|
|
1310
|
+
participantMap.off("participantsUpdate", this.onParticipantsUpdate);
|
|
1311
|
+
this.joinedMapSubscriptionsAttached = false;
|
|
1312
|
+
}
|
|
1313
|
+
syncAllParticipants() {
|
|
1314
|
+
const participantMap = this.getParticipantMap();
|
|
1315
|
+
if (!participantMap || !this.meeting || !this.options.autoSubscribe) {
|
|
1316
|
+
return;
|
|
1317
|
+
}
|
|
1318
|
+
for (const participant of participantMap.values()) {
|
|
1319
|
+
this.attachParticipant(participant);
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
getParticipantMap() {
|
|
1323
|
+
if (!this.meeting) {
|
|
1324
|
+
return null;
|
|
1325
|
+
}
|
|
1326
|
+
return this.meeting.participants.active ?? this.meeting.participants.joined ?? null;
|
|
1327
|
+
}
|
|
1328
|
+
attachParticipant(participant) {
|
|
1329
|
+
if (!this.options.autoSubscribe || !this.meeting) {
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
if (participant.id === this.meeting.self.id || this.participantListeners.has(participant.id)) {
|
|
1333
|
+
this.syncParticipantTracks(participant);
|
|
1334
|
+
return;
|
|
1335
|
+
}
|
|
1336
|
+
const listenerSet = {
|
|
1337
|
+
participant,
|
|
1338
|
+
onAudioUpdate: ({ audioEnabled, audioTrack }) => {
|
|
1339
|
+
this.handleRemoteTrackUpdate("audio", participant, audioTrack, audioEnabled);
|
|
1340
|
+
},
|
|
1341
|
+
onVideoUpdate: ({ videoEnabled, videoTrack }) => {
|
|
1342
|
+
this.handleRemoteTrackUpdate("video", participant, videoTrack, videoEnabled);
|
|
1343
|
+
},
|
|
1344
|
+
onScreenShareUpdate: ({ screenShareEnabled, screenShareTracks }) => {
|
|
1345
|
+
this.handleRemoteTrackUpdate("screen", participant, screenShareTracks.video, screenShareEnabled);
|
|
1346
|
+
}
|
|
1347
|
+
};
|
|
1348
|
+
participant.on("audioUpdate", listenerSet.onAudioUpdate);
|
|
1349
|
+
participant.on("videoUpdate", listenerSet.onVideoUpdate);
|
|
1350
|
+
participant.on("screenShareUpdate", listenerSet.onScreenShareUpdate);
|
|
1351
|
+
this.participantListeners.set(participant.id, listenerSet);
|
|
1352
|
+
this.syncParticipantTracks(participant);
|
|
1353
|
+
}
|
|
1354
|
+
detachParticipant(participantId) {
|
|
1355
|
+
const listenerSet = this.participantListeners.get(participantId);
|
|
1356
|
+
if (!listenerSet) return;
|
|
1357
|
+
listenerSet.participant.off("audioUpdate", listenerSet.onAudioUpdate);
|
|
1358
|
+
listenerSet.participant.off("videoUpdate", listenerSet.onVideoUpdate);
|
|
1359
|
+
listenerSet.participant.off("screenShareUpdate", listenerSet.onScreenShareUpdate);
|
|
1360
|
+
this.participantListeners.delete(participantId);
|
|
1361
|
+
for (const kind of ["audio", "video", "screen"]) {
|
|
1362
|
+
this.remoteTrackIds.delete(buildRemoteTrackKey(participantId, kind));
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
clearParticipantListeners() {
|
|
1366
|
+
for (const participantId of Array.from(this.participantListeners.keys())) {
|
|
1367
|
+
this.detachParticipant(participantId);
|
|
1368
|
+
}
|
|
1369
|
+
this.remoteTrackIds.clear();
|
|
1370
|
+
}
|
|
1371
|
+
syncParticipantTracks(participant) {
|
|
1372
|
+
this.handleRemoteTrackUpdate("audio", participant, participant.audioTrack, participant.audioEnabled === true);
|
|
1373
|
+
this.handleRemoteTrackUpdate("video", participant, participant.videoTrack, participant.videoEnabled === true);
|
|
1374
|
+
this.handleRemoteTrackUpdate("screen", participant, participant.screenShareTracks?.video, participant.screenShareEnabled === true);
|
|
1375
|
+
}
|
|
1376
|
+
handleRemoteTrackUpdate(kind, participant, track, enabled) {
|
|
1377
|
+
const key = buildRemoteTrackKey(participant.id, kind);
|
|
1378
|
+
if (!enabled || !track) {
|
|
1379
|
+
this.remoteTrackIds.delete(key);
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1382
|
+
const previousTrackId = this.remoteTrackIds.get(key);
|
|
1383
|
+
if (previousTrackId === track.id) {
|
|
1384
|
+
return;
|
|
1385
|
+
}
|
|
1386
|
+
this.remoteTrackIds.set(key, track.id);
|
|
1387
|
+
const payload = {
|
|
1388
|
+
kind,
|
|
1389
|
+
track,
|
|
1390
|
+
stream: new MediaStream([track]),
|
|
1391
|
+
trackName: track.id,
|
|
1392
|
+
providerSessionId: participant.id,
|
|
1393
|
+
participantId: participant.id,
|
|
1394
|
+
customParticipantId: participant.customParticipantId,
|
|
1395
|
+
userId: participant.userId
|
|
1396
|
+
};
|
|
1397
|
+
for (const handler of this.remoteTrackHandlers) {
|
|
1398
|
+
handler(payload);
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
cleanupMeeting() {
|
|
1402
|
+
const meeting = this.meeting;
|
|
1403
|
+
this.detachParticipantMapListeners();
|
|
1404
|
+
this.clearParticipantListeners();
|
|
1405
|
+
this.meeting = null;
|
|
1406
|
+
this.sessionId = null;
|
|
1407
|
+
this.leaveMeetingSilently(meeting);
|
|
1408
|
+
}
|
|
1409
|
+
assertConnectStillActive(lifecycleVersion, meeting) {
|
|
1410
|
+
if (lifecycleVersion === this.lifecycleVersion) {
|
|
1411
|
+
return;
|
|
1412
|
+
}
|
|
1413
|
+
if (meeting) {
|
|
1414
|
+
this.leaveMeetingSilently(meeting);
|
|
1415
|
+
}
|
|
1416
|
+
throw new Error("Cloudflare media transport was destroyed during connect.");
|
|
1417
|
+
}
|
|
1418
|
+
leaveMeetingSilently(meeting) {
|
|
1419
|
+
if (!meeting) {
|
|
1420
|
+
return;
|
|
1421
|
+
}
|
|
1422
|
+
const leavePromise = meeting.leave?.() ?? meeting.leaveRoom?.();
|
|
1423
|
+
if (leavePromise) {
|
|
1424
|
+
void leavePromise.catch(() => {
|
|
1425
|
+
});
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
};
|
|
1429
|
+
|
|
1430
|
+
// src/room-p2p-media.ts
|
|
1431
|
+
var DEFAULT_SIGNAL_PREFIX = "edgebase.media.p2p";
|
|
1432
|
+
var DEFAULT_ICE_SERVERS = [
|
|
1433
|
+
{ urls: "stun:stun.l.google.com:19302" }
|
|
1434
|
+
];
|
|
1435
|
+
var DEFAULT_MEMBER_READY_TIMEOUT_MS = 1e4;
|
|
1436
|
+
function buildTrackKey(memberId, trackId) {
|
|
1437
|
+
return `${memberId}:${trackId}`;
|
|
1438
|
+
}
|
|
1439
|
+
function buildExactDeviceConstraint(deviceId) {
|
|
1440
|
+
return { deviceId: { exact: deviceId } };
|
|
1441
|
+
}
|
|
1442
|
+
function normalizeTrackKind(track) {
|
|
1443
|
+
if (track.kind === "audio") return "audio";
|
|
1444
|
+
if (track.kind === "video") return "video";
|
|
1445
|
+
return null;
|
|
1446
|
+
}
|
|
1447
|
+
function serializeDescription(description) {
|
|
1448
|
+
return {
|
|
1449
|
+
type: description.type,
|
|
1450
|
+
sdp: description.sdp ?? void 0
|
|
1451
|
+
};
|
|
1452
|
+
}
|
|
1453
|
+
function serializeCandidate(candidate) {
|
|
1454
|
+
if ("toJSON" in candidate && typeof candidate.toJSON === "function") {
|
|
1455
|
+
return candidate.toJSON();
|
|
1456
|
+
}
|
|
1457
|
+
return candidate;
|
|
1458
|
+
}
|
|
1459
|
+
function installMessage2(packageName) {
|
|
1460
|
+
return `Install ${packageName} to use React Native P2P media transport. See https://edgebase.fun/docs/room/media`;
|
|
1461
|
+
}
|
|
1462
|
+
var RoomP2PMediaTransport = class {
|
|
1463
|
+
room;
|
|
1464
|
+
options;
|
|
1465
|
+
localTracks = /* @__PURE__ */ new Map();
|
|
1466
|
+
peers = /* @__PURE__ */ new Map();
|
|
1467
|
+
remoteTrackHandlers = [];
|
|
1468
|
+
remoteTrackKinds = /* @__PURE__ */ new Map();
|
|
1469
|
+
emittedRemoteTracks = /* @__PURE__ */ new Set();
|
|
1470
|
+
pendingRemoteTracks = /* @__PURE__ */ new Map();
|
|
1471
|
+
subscriptions = [];
|
|
1472
|
+
localMemberId = null;
|
|
1473
|
+
connected = false;
|
|
1474
|
+
runtimePromise = null;
|
|
1475
|
+
constructor(room, options) {
|
|
1476
|
+
this.room = room;
|
|
1477
|
+
this.options = {
|
|
1478
|
+
rtcConfiguration: {
|
|
1479
|
+
...options?.rtcConfiguration,
|
|
1480
|
+
iceServers: options?.rtcConfiguration?.iceServers && options.rtcConfiguration.iceServers.length > 0 ? options.rtcConfiguration.iceServers : DEFAULT_ICE_SERVERS
|
|
1481
|
+
},
|
|
1482
|
+
peerConnectionFactory: options?.peerConnectionFactory,
|
|
1483
|
+
mediaDevices: options?.mediaDevices,
|
|
1484
|
+
signalPrefix: options?.signalPrefix ?? DEFAULT_SIGNAL_PREFIX
|
|
1485
|
+
};
|
|
1486
|
+
}
|
|
1487
|
+
getSessionId() {
|
|
1488
|
+
return this.localMemberId;
|
|
1489
|
+
}
|
|
1490
|
+
getPeerConnection() {
|
|
1491
|
+
if (this.peers.size !== 1) {
|
|
1492
|
+
return null;
|
|
1493
|
+
}
|
|
1494
|
+
return this.peers.values().next().value?.pc ?? null;
|
|
1495
|
+
}
|
|
1496
|
+
async connect(payload) {
|
|
1497
|
+
if (this.connected && this.localMemberId) {
|
|
1498
|
+
return this.localMemberId;
|
|
1499
|
+
}
|
|
1500
|
+
if (payload && typeof payload === "object" && "sessionDescription" in payload) {
|
|
1501
|
+
throw new Error(
|
|
1502
|
+
"RoomP2PMediaTransport.connect() does not accept sessionDescription; use room.signals through the built-in transport instead."
|
|
1503
|
+
);
|
|
1504
|
+
}
|
|
1505
|
+
await this.ensureRuntime();
|
|
1506
|
+
const currentMember = await this.waitForCurrentMember();
|
|
1507
|
+
if (!currentMember) {
|
|
1508
|
+
throw new Error("Join the room before connecting a P2P media transport.");
|
|
1509
|
+
}
|
|
1510
|
+
this.localMemberId = currentMember.memberId;
|
|
1511
|
+
this.connected = true;
|
|
1512
|
+
this.hydrateRemoteTrackKinds();
|
|
1513
|
+
this.attachRoomSubscriptions();
|
|
1514
|
+
try {
|
|
1515
|
+
for (const member of this.room.members.list()) {
|
|
1516
|
+
if (member.memberId !== this.localMemberId) {
|
|
1517
|
+
this.ensurePeer(member.memberId);
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
} catch (error) {
|
|
1521
|
+
this.rollbackConnectedState();
|
|
1522
|
+
throw error;
|
|
1523
|
+
}
|
|
1524
|
+
return this.localMemberId;
|
|
1525
|
+
}
|
|
1526
|
+
async waitForCurrentMember(timeoutMs = DEFAULT_MEMBER_READY_TIMEOUT_MS) {
|
|
1527
|
+
const startedAt = Date.now();
|
|
1528
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
1529
|
+
const member = this.room.members.current();
|
|
1530
|
+
if (member) {
|
|
1531
|
+
return member;
|
|
1532
|
+
}
|
|
1533
|
+
await new Promise((resolve) => globalThis.setTimeout(resolve, 50));
|
|
1534
|
+
}
|
|
1535
|
+
return this.room.members.current();
|
|
1536
|
+
}
|
|
1537
|
+
async enableAudio(constraints = true) {
|
|
1538
|
+
const track = await this.createUserMediaTrack("audio", constraints);
|
|
1539
|
+
if (!track) {
|
|
1540
|
+
throw new Error("P2P transport could not create a local audio track.");
|
|
1541
|
+
}
|
|
1542
|
+
const providerSessionId = await this.ensureConnectedMemberId();
|
|
1543
|
+
this.rememberLocalTrack("audio", track, track.getSettings().deviceId, true);
|
|
1544
|
+
await this.room.media.audio.enable?.({
|
|
1545
|
+
trackId: track.id,
|
|
1546
|
+
deviceId: track.getSettings().deviceId,
|
|
1547
|
+
providerSessionId
|
|
1548
|
+
});
|
|
1549
|
+
this.syncAllPeerSenders();
|
|
1550
|
+
return track;
|
|
1551
|
+
}
|
|
1552
|
+
async enableVideo(constraints = true) {
|
|
1553
|
+
const track = await this.createUserMediaTrack("video", constraints);
|
|
1554
|
+
if (!track) {
|
|
1555
|
+
throw new Error("P2P transport could not create a local video track.");
|
|
1556
|
+
}
|
|
1557
|
+
const providerSessionId = await this.ensureConnectedMemberId();
|
|
1558
|
+
this.rememberLocalTrack("video", track, track.getSettings().deviceId, true);
|
|
1559
|
+
await this.room.media.video.enable?.({
|
|
1560
|
+
trackId: track.id,
|
|
1561
|
+
deviceId: track.getSettings().deviceId,
|
|
1562
|
+
providerSessionId
|
|
1563
|
+
});
|
|
1564
|
+
this.syncAllPeerSenders();
|
|
1565
|
+
return track;
|
|
1566
|
+
}
|
|
1567
|
+
async startScreenShare(constraints = { video: true, audio: false }) {
|
|
1568
|
+
const devices = await this.resolveMediaDevices();
|
|
1569
|
+
if (!devices?.getDisplayMedia) {
|
|
1570
|
+
throw new Error("Screen sharing is not available in this environment.");
|
|
1571
|
+
}
|
|
1572
|
+
const stream = await devices.getDisplayMedia(constraints);
|
|
1573
|
+
const track = stream.getVideoTracks()[0] ?? null;
|
|
1574
|
+
if (!track) {
|
|
1575
|
+
throw new Error("P2P transport could not create a screen-share track.");
|
|
1576
|
+
}
|
|
1577
|
+
track.addEventListener("ended", () => {
|
|
1578
|
+
void this.stopScreenShare();
|
|
1579
|
+
}, { once: true });
|
|
1580
|
+
const providerSessionId = await this.ensureConnectedMemberId();
|
|
1581
|
+
this.rememberLocalTrack("screen", track, track.getSettings().deviceId, true);
|
|
1582
|
+
await this.room.media.screen.start?.({
|
|
1583
|
+
trackId: track.id,
|
|
1584
|
+
deviceId: track.getSettings().deviceId,
|
|
1585
|
+
providerSessionId
|
|
1586
|
+
});
|
|
1587
|
+
this.syncAllPeerSenders();
|
|
1588
|
+
return track;
|
|
1589
|
+
}
|
|
1590
|
+
async disableAudio() {
|
|
1591
|
+
this.releaseLocalTrack("audio");
|
|
1592
|
+
this.syncAllPeerSenders();
|
|
1593
|
+
await this.room.media.audio.disable();
|
|
1594
|
+
}
|
|
1595
|
+
async disableVideo() {
|
|
1596
|
+
this.releaseLocalTrack("video");
|
|
1597
|
+
this.syncAllPeerSenders();
|
|
1598
|
+
await this.room.media.video.disable();
|
|
1599
|
+
}
|
|
1600
|
+
async stopScreenShare() {
|
|
1601
|
+
this.releaseLocalTrack("screen");
|
|
1602
|
+
this.syncAllPeerSenders();
|
|
1603
|
+
await this.room.media.screen.stop();
|
|
1604
|
+
}
|
|
1605
|
+
async setMuted(kind, muted) {
|
|
1606
|
+
const localTrack = this.localTracks.get(kind)?.track;
|
|
1607
|
+
if (localTrack) {
|
|
1608
|
+
localTrack.enabled = !muted;
|
|
1609
|
+
}
|
|
1610
|
+
if (kind === "audio") {
|
|
1611
|
+
await this.room.media.audio.setMuted?.(muted);
|
|
1612
|
+
} else {
|
|
1613
|
+
await this.room.media.video.setMuted?.(muted);
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
async switchDevices(payload) {
|
|
1617
|
+
if (payload.audioInputId && this.localTracks.has("audio")) {
|
|
1618
|
+
const nextAudioTrack = await this.createUserMediaTrack("audio", buildExactDeviceConstraint(payload.audioInputId));
|
|
1619
|
+
if (nextAudioTrack) {
|
|
1620
|
+
this.rememberLocalTrack("audio", nextAudioTrack, payload.audioInputId, true);
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
if (payload.videoInputId && this.localTracks.has("video")) {
|
|
1624
|
+
const nextVideoTrack = await this.createUserMediaTrack("video", buildExactDeviceConstraint(payload.videoInputId));
|
|
1625
|
+
if (nextVideoTrack) {
|
|
1626
|
+
this.rememberLocalTrack("video", nextVideoTrack, payload.videoInputId, true);
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
this.syncAllPeerSenders();
|
|
1630
|
+
await this.room.media.devices.switch(payload);
|
|
1631
|
+
}
|
|
1632
|
+
onRemoteTrack(handler) {
|
|
1633
|
+
this.remoteTrackHandlers.push(handler);
|
|
1634
|
+
return core.createSubscription(() => {
|
|
1635
|
+
const index = this.remoteTrackHandlers.indexOf(handler);
|
|
1636
|
+
if (index >= 0) {
|
|
1637
|
+
this.remoteTrackHandlers.splice(index, 1);
|
|
1638
|
+
}
|
|
1639
|
+
});
|
|
1640
|
+
}
|
|
1641
|
+
destroy() {
|
|
1642
|
+
this.connected = false;
|
|
1643
|
+
this.localMemberId = null;
|
|
1644
|
+
for (const subscription of this.subscriptions.splice(0)) {
|
|
1645
|
+
subscription.unsubscribe();
|
|
1646
|
+
}
|
|
1647
|
+
for (const peer of this.peers.values()) {
|
|
1648
|
+
this.destroyPeer(peer);
|
|
1649
|
+
}
|
|
1650
|
+
this.peers.clear();
|
|
1651
|
+
for (const kind of Array.from(this.localTracks.keys())) {
|
|
1652
|
+
this.releaseLocalTrack(kind);
|
|
1653
|
+
}
|
|
1654
|
+
this.remoteTrackKinds.clear();
|
|
1655
|
+
this.emittedRemoteTracks.clear();
|
|
1656
|
+
this.pendingRemoteTracks.clear();
|
|
1657
|
+
}
|
|
1658
|
+
attachRoomSubscriptions() {
|
|
1659
|
+
if (this.subscriptions.length > 0) {
|
|
1660
|
+
return;
|
|
1661
|
+
}
|
|
1662
|
+
this.subscriptions.push(
|
|
1663
|
+
this.room.members.onJoin((member) => {
|
|
1664
|
+
if (member.memberId !== this.localMemberId) {
|
|
1665
|
+
this.ensurePeer(member.memberId);
|
|
1666
|
+
}
|
|
1667
|
+
}),
|
|
1668
|
+
this.room.members.onSync((members) => {
|
|
1669
|
+
const activeMemberIds = /* @__PURE__ */ new Set();
|
|
1670
|
+
for (const member of members) {
|
|
1671
|
+
if (member.memberId !== this.localMemberId) {
|
|
1672
|
+
activeMemberIds.add(member.memberId);
|
|
1673
|
+
this.ensurePeer(member.memberId);
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
for (const memberId of Array.from(this.peers.keys())) {
|
|
1677
|
+
if (!activeMemberIds.has(memberId)) {
|
|
1678
|
+
this.removeRemoteMember(memberId);
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
}),
|
|
1682
|
+
this.room.members.onLeave((member) => {
|
|
1683
|
+
this.removeRemoteMember(member.memberId);
|
|
1684
|
+
}),
|
|
1685
|
+
this.room.signals.on(this.offerEvent, (payload, meta) => {
|
|
1686
|
+
void this.handleDescriptionSignal("offer", payload, meta);
|
|
1687
|
+
}),
|
|
1688
|
+
this.room.signals.on(this.answerEvent, (payload, meta) => {
|
|
1689
|
+
void this.handleDescriptionSignal("answer", payload, meta);
|
|
1690
|
+
}),
|
|
1691
|
+
this.room.signals.on(this.iceEvent, (payload, meta) => {
|
|
1692
|
+
void this.handleIceSignal(payload, meta);
|
|
1693
|
+
}),
|
|
1694
|
+
this.room.media.onTrack((track, member) => {
|
|
1695
|
+
if (member.memberId !== this.localMemberId) {
|
|
1696
|
+
this.ensurePeer(member.memberId);
|
|
1697
|
+
}
|
|
1698
|
+
this.rememberRemoteTrackKind(track, member);
|
|
1699
|
+
}),
|
|
1700
|
+
this.room.media.onTrackRemoved((track, member) => {
|
|
1701
|
+
if (!track.trackId) return;
|
|
1702
|
+
const key = buildTrackKey(member.memberId, track.trackId);
|
|
1703
|
+
this.remoteTrackKinds.delete(key);
|
|
1704
|
+
this.emittedRemoteTracks.delete(key);
|
|
1705
|
+
this.pendingRemoteTracks.delete(key);
|
|
1706
|
+
})
|
|
1707
|
+
);
|
|
1708
|
+
}
|
|
1709
|
+
hydrateRemoteTrackKinds() {
|
|
1710
|
+
this.remoteTrackKinds.clear();
|
|
1711
|
+
this.emittedRemoteTracks.clear();
|
|
1712
|
+
this.pendingRemoteTracks.clear();
|
|
1713
|
+
for (const mediaMember of this.room.media.list()) {
|
|
1714
|
+
for (const track of mediaMember.tracks) {
|
|
1715
|
+
this.rememberRemoteTrackKind(track, mediaMember.member);
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
rememberRemoteTrackKind(track, member) {
|
|
1720
|
+
if (!track.trackId || member.memberId === this.localMemberId) {
|
|
1721
|
+
return;
|
|
1722
|
+
}
|
|
1723
|
+
const key = buildTrackKey(member.memberId, track.trackId);
|
|
1724
|
+
this.remoteTrackKinds.set(key, track.kind);
|
|
1725
|
+
const pending = this.pendingRemoteTracks.get(key);
|
|
1726
|
+
if (pending) {
|
|
1727
|
+
this.pendingRemoteTracks.delete(key);
|
|
1728
|
+
this.emitRemoteTrack(member.memberId, pending.track, pending.stream, track.kind);
|
|
1729
|
+
return;
|
|
1730
|
+
}
|
|
1731
|
+
this.flushPendingRemoteTracks(member.memberId, track.kind);
|
|
1732
|
+
}
|
|
1733
|
+
ensurePeer(memberId) {
|
|
1734
|
+
const existing = this.peers.get(memberId);
|
|
1735
|
+
if (existing) {
|
|
1736
|
+
this.syncPeerSenders(existing);
|
|
1737
|
+
return existing;
|
|
1738
|
+
}
|
|
1739
|
+
const factory = this.options.peerConnectionFactory ?? ((configuration) => new RTCPeerConnection(configuration));
|
|
1740
|
+
const pc = factory(this.options.rtcConfiguration);
|
|
1741
|
+
const peer = {
|
|
1742
|
+
memberId,
|
|
1743
|
+
pc,
|
|
1744
|
+
polite: !!this.localMemberId && this.localMemberId.localeCompare(memberId) > 0,
|
|
1745
|
+
makingOffer: false,
|
|
1746
|
+
ignoreOffer: false,
|
|
1747
|
+
isSettingRemoteAnswerPending: false,
|
|
1748
|
+
pendingCandidates: [],
|
|
1749
|
+
senders: /* @__PURE__ */ new Map()
|
|
1750
|
+
};
|
|
1751
|
+
pc.onicecandidate = (event) => {
|
|
1752
|
+
if (!event.candidate) return;
|
|
1753
|
+
void this.room.signals.sendTo(memberId, this.iceEvent, {
|
|
1754
|
+
candidate: serializeCandidate(event.candidate)
|
|
1755
|
+
});
|
|
1756
|
+
};
|
|
1757
|
+
pc.onnegotiationneeded = () => {
|
|
1758
|
+
void this.negotiatePeer(peer);
|
|
1759
|
+
};
|
|
1760
|
+
pc.ontrack = (event) => {
|
|
1761
|
+
const stream = event.streams[0] ?? new MediaStream([event.track]);
|
|
1762
|
+
const key = buildTrackKey(memberId, event.track.id);
|
|
1763
|
+
const exactKind = this.remoteTrackKinds.get(key);
|
|
1764
|
+
const fallbackKind = exactKind ? null : this.resolveFallbackRemoteTrackKind(memberId, event.track);
|
|
1765
|
+
const kind = exactKind ?? fallbackKind ?? normalizeTrackKind(event.track);
|
|
1766
|
+
if (!kind || !exactKind && !fallbackKind && kind === "video" && event.track.kind === "video") {
|
|
1767
|
+
this.pendingRemoteTracks.set(key, { memberId, track: event.track, stream });
|
|
1768
|
+
return;
|
|
1769
|
+
}
|
|
1770
|
+
this.emitRemoteTrack(memberId, event.track, stream, kind);
|
|
1771
|
+
};
|
|
1772
|
+
this.peers.set(memberId, peer);
|
|
1773
|
+
this.syncPeerSenders(peer);
|
|
1774
|
+
return peer;
|
|
1775
|
+
}
|
|
1776
|
+
async negotiatePeer(peer) {
|
|
1777
|
+
if (!this.connected || peer.pc.connectionState === "closed" || peer.makingOffer || peer.isSettingRemoteAnswerPending || peer.pc.signalingState !== "stable") {
|
|
1778
|
+
return;
|
|
1779
|
+
}
|
|
1780
|
+
try {
|
|
1781
|
+
peer.makingOffer = true;
|
|
1782
|
+
await peer.pc.setLocalDescription();
|
|
1783
|
+
if (!peer.pc.localDescription) {
|
|
1784
|
+
return;
|
|
1785
|
+
}
|
|
1786
|
+
await this.room.signals.sendTo(peer.memberId, this.offerEvent, {
|
|
1787
|
+
description: serializeDescription(peer.pc.localDescription)
|
|
1788
|
+
});
|
|
1789
|
+
} catch (error) {
|
|
1790
|
+
console.warn("[RoomP2PMediaTransport] Failed to negotiate peer offer.", {
|
|
1791
|
+
memberId: peer.memberId,
|
|
1792
|
+
signalingState: peer.pc.signalingState,
|
|
1793
|
+
error
|
|
1794
|
+
});
|
|
1795
|
+
} finally {
|
|
1796
|
+
peer.makingOffer = false;
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
async handleDescriptionSignal(expectedType, payload, meta) {
|
|
1800
|
+
const senderId = typeof meta.memberId === "string" && meta.memberId.trim() ? meta.memberId.trim() : "";
|
|
1801
|
+
if (!senderId || senderId === this.localMemberId) {
|
|
1802
|
+
return;
|
|
1803
|
+
}
|
|
1804
|
+
const description = this.normalizeDescription(payload);
|
|
1805
|
+
if (!description || description.type !== expectedType) {
|
|
1806
|
+
return;
|
|
1807
|
+
}
|
|
1808
|
+
const peer = this.ensurePeer(senderId);
|
|
1809
|
+
const readyForOffer = !peer.makingOffer && (peer.pc.signalingState === "stable" || peer.isSettingRemoteAnswerPending);
|
|
1810
|
+
const offerCollision = description.type === "offer" && !readyForOffer;
|
|
1811
|
+
peer.ignoreOffer = !peer.polite && offerCollision;
|
|
1812
|
+
if (peer.ignoreOffer) {
|
|
1813
|
+
return;
|
|
1814
|
+
}
|
|
1815
|
+
try {
|
|
1816
|
+
peer.isSettingRemoteAnswerPending = description.type === "answer";
|
|
1817
|
+
await peer.pc.setRemoteDescription(description);
|
|
1818
|
+
peer.isSettingRemoteAnswerPending = false;
|
|
1819
|
+
await this.flushPendingCandidates(peer);
|
|
1820
|
+
if (description.type === "offer") {
|
|
1821
|
+
this.syncPeerSenders(peer);
|
|
1822
|
+
await peer.pc.setLocalDescription();
|
|
1823
|
+
if (!peer.pc.localDescription) {
|
|
1824
|
+
return;
|
|
1825
|
+
}
|
|
1826
|
+
await this.room.signals.sendTo(senderId, this.answerEvent, {
|
|
1827
|
+
description: serializeDescription(peer.pc.localDescription)
|
|
1828
|
+
});
|
|
1829
|
+
}
|
|
1830
|
+
} catch (error) {
|
|
1831
|
+
console.warn("[RoomP2PMediaTransport] Failed to apply remote session description.", {
|
|
1832
|
+
memberId: senderId,
|
|
1833
|
+
expectedType,
|
|
1834
|
+
signalingState: peer.pc.signalingState,
|
|
1835
|
+
error
|
|
1836
|
+
});
|
|
1837
|
+
peer.isSettingRemoteAnswerPending = false;
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
async handleIceSignal(payload, meta) {
|
|
1841
|
+
const senderId = typeof meta.memberId === "string" && meta.memberId.trim() ? meta.memberId.trim() : "";
|
|
1842
|
+
if (!senderId || senderId === this.localMemberId) {
|
|
1843
|
+
return;
|
|
1844
|
+
}
|
|
1845
|
+
const candidate = this.normalizeCandidate(payload);
|
|
1846
|
+
if (!candidate) {
|
|
1847
|
+
return;
|
|
1848
|
+
}
|
|
1849
|
+
const peer = this.ensurePeer(senderId);
|
|
1850
|
+
if (!peer.pc.remoteDescription) {
|
|
1851
|
+
peer.pendingCandidates.push(candidate);
|
|
1852
|
+
return;
|
|
1853
|
+
}
|
|
1854
|
+
try {
|
|
1855
|
+
await peer.pc.addIceCandidate(candidate);
|
|
1856
|
+
} catch (error) {
|
|
1857
|
+
console.warn("[RoomP2PMediaTransport] Failed to add ICE candidate.", {
|
|
1858
|
+
memberId: senderId,
|
|
1859
|
+
error
|
|
1860
|
+
});
|
|
1861
|
+
if (!peer.ignoreOffer) {
|
|
1862
|
+
peer.pendingCandidates.push(candidate);
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
async flushPendingCandidates(peer) {
|
|
1867
|
+
if (!peer.pc.remoteDescription || peer.pendingCandidates.length === 0) {
|
|
1868
|
+
return;
|
|
1869
|
+
}
|
|
1870
|
+
const pending = [...peer.pendingCandidates];
|
|
1871
|
+
peer.pendingCandidates.length = 0;
|
|
1872
|
+
for (const candidate of pending) {
|
|
1873
|
+
try {
|
|
1874
|
+
await peer.pc.addIceCandidate(candidate);
|
|
1875
|
+
} catch (error) {
|
|
1876
|
+
console.warn("[RoomP2PMediaTransport] Failed to flush pending ICE candidate.", {
|
|
1877
|
+
memberId: peer.memberId,
|
|
1878
|
+
error
|
|
1879
|
+
});
|
|
1880
|
+
if (!peer.ignoreOffer) {
|
|
1881
|
+
peer.pendingCandidates.push(candidate);
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
syncAllPeerSenders() {
|
|
1887
|
+
for (const peer of this.peers.values()) {
|
|
1888
|
+
this.syncPeerSenders(peer);
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
syncPeerSenders(peer) {
|
|
1892
|
+
const activeKinds = /* @__PURE__ */ new Set();
|
|
1893
|
+
let changed = false;
|
|
1894
|
+
for (const [kind, localTrack] of this.localTracks.entries()) {
|
|
1895
|
+
activeKinds.add(kind);
|
|
1896
|
+
const sender = peer.senders.get(kind);
|
|
1897
|
+
if (sender) {
|
|
1898
|
+
if (sender.track !== localTrack.track) {
|
|
1899
|
+
void sender.replaceTrack(localTrack.track);
|
|
1900
|
+
changed = true;
|
|
1901
|
+
}
|
|
1902
|
+
continue;
|
|
1903
|
+
}
|
|
1904
|
+
const addedSender = peer.pc.addTrack(localTrack.track, new MediaStream([localTrack.track]));
|
|
1905
|
+
peer.senders.set(kind, addedSender);
|
|
1906
|
+
changed = true;
|
|
1907
|
+
}
|
|
1908
|
+
for (const [kind, sender] of Array.from(peer.senders.entries())) {
|
|
1909
|
+
if (activeKinds.has(kind)) {
|
|
1910
|
+
continue;
|
|
1911
|
+
}
|
|
1912
|
+
try {
|
|
1913
|
+
peer.pc.removeTrack(sender);
|
|
1914
|
+
} catch {
|
|
1915
|
+
}
|
|
1916
|
+
peer.senders.delete(kind);
|
|
1917
|
+
changed = true;
|
|
1918
|
+
}
|
|
1919
|
+
if (changed) {
|
|
1920
|
+
void this.negotiatePeer(peer);
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
emitRemoteTrack(memberId, track, stream, kind) {
|
|
1924
|
+
const key = buildTrackKey(memberId, track.id);
|
|
1925
|
+
if (this.emittedRemoteTracks.has(key)) {
|
|
1926
|
+
return;
|
|
1927
|
+
}
|
|
1928
|
+
this.emittedRemoteTracks.add(key);
|
|
1929
|
+
this.remoteTrackKinds.set(key, kind);
|
|
1930
|
+
const participant = this.findMember(memberId);
|
|
1931
|
+
const payload = {
|
|
1932
|
+
kind,
|
|
1933
|
+
track,
|
|
1934
|
+
stream,
|
|
1935
|
+
trackName: track.id,
|
|
1936
|
+
providerSessionId: memberId,
|
|
1937
|
+
participantId: memberId,
|
|
1938
|
+
userId: participant?.userId
|
|
1939
|
+
};
|
|
1940
|
+
for (const handler of this.remoteTrackHandlers) {
|
|
1941
|
+
handler(payload);
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
resolveFallbackRemoteTrackKind(memberId, track) {
|
|
1945
|
+
const normalizedKind = normalizeTrackKind(track);
|
|
1946
|
+
if (!normalizedKind) {
|
|
1947
|
+
return null;
|
|
1948
|
+
}
|
|
1949
|
+
if (normalizedKind === "audio") {
|
|
1950
|
+
return "audio";
|
|
1951
|
+
}
|
|
1952
|
+
return this.getNextUnassignedPublishedVideoLikeKind(memberId);
|
|
1953
|
+
}
|
|
1954
|
+
flushPendingRemoteTracks(memberId, roomKind) {
|
|
1955
|
+
const expectedTrackKind = roomKind === "audio" ? "audio" : "video";
|
|
1956
|
+
for (const [key, pending] of this.pendingRemoteTracks.entries()) {
|
|
1957
|
+
if (pending.memberId !== memberId || pending.track.kind !== expectedTrackKind) {
|
|
1958
|
+
continue;
|
|
1959
|
+
}
|
|
1960
|
+
this.pendingRemoteTracks.delete(key);
|
|
1961
|
+
this.emitRemoteTrack(memberId, pending.track, pending.stream, roomKind);
|
|
1962
|
+
return;
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
getPublishedVideoLikeKinds(memberId) {
|
|
1966
|
+
const mediaMember = this.room.media.list().find((entry) => entry.member.memberId === memberId);
|
|
1967
|
+
if (!mediaMember) {
|
|
1968
|
+
return [];
|
|
1969
|
+
}
|
|
1970
|
+
const publishedKinds = /* @__PURE__ */ new Set();
|
|
1971
|
+
for (const track of mediaMember.tracks) {
|
|
1972
|
+
if ((track.kind === "video" || track.kind === "screen") && track.trackId) {
|
|
1973
|
+
publishedKinds.add(track.kind);
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
return Array.from(publishedKinds);
|
|
1977
|
+
}
|
|
1978
|
+
getNextUnassignedPublishedVideoLikeKind(memberId) {
|
|
1979
|
+
const publishedKinds = this.getPublishedVideoLikeKinds(memberId);
|
|
1980
|
+
if (publishedKinds.length === 0) {
|
|
1981
|
+
return null;
|
|
1982
|
+
}
|
|
1983
|
+
const assignedKinds = /* @__PURE__ */ new Set();
|
|
1984
|
+
for (const key of this.emittedRemoteTracks) {
|
|
1985
|
+
if (!key.startsWith(`${memberId}:`)) {
|
|
1986
|
+
continue;
|
|
1987
|
+
}
|
|
1988
|
+
const kind = this.remoteTrackKinds.get(key);
|
|
1989
|
+
if (kind === "video" || kind === "screen") {
|
|
1990
|
+
assignedKinds.add(kind);
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
return publishedKinds.find((kind) => !assignedKinds.has(kind)) ?? null;
|
|
1994
|
+
}
|
|
1995
|
+
closePeer(memberId) {
|
|
1996
|
+
const peer = this.peers.get(memberId);
|
|
1997
|
+
if (!peer) return;
|
|
1998
|
+
this.destroyPeer(peer);
|
|
1999
|
+
this.peers.delete(memberId);
|
|
2000
|
+
}
|
|
2001
|
+
removeRemoteMember(memberId) {
|
|
2002
|
+
this.remoteTrackKinds.forEach((_kind, key) => {
|
|
2003
|
+
if (key.startsWith(`${memberId}:`)) {
|
|
2004
|
+
this.remoteTrackKinds.delete(key);
|
|
2005
|
+
}
|
|
2006
|
+
});
|
|
2007
|
+
this.emittedRemoteTracks.forEach((key) => {
|
|
2008
|
+
if (key.startsWith(`${memberId}:`)) {
|
|
2009
|
+
this.emittedRemoteTracks.delete(key);
|
|
2010
|
+
}
|
|
2011
|
+
});
|
|
2012
|
+
this.pendingRemoteTracks.forEach((_pending, key) => {
|
|
2013
|
+
if (key.startsWith(`${memberId}:`)) {
|
|
2014
|
+
this.pendingRemoteTracks.delete(key);
|
|
2015
|
+
}
|
|
2016
|
+
});
|
|
2017
|
+
this.closePeer(memberId);
|
|
2018
|
+
}
|
|
2019
|
+
findMember(memberId) {
|
|
2020
|
+
return this.room.members.list().find((member) => member.memberId === memberId);
|
|
2021
|
+
}
|
|
2022
|
+
rollbackConnectedState() {
|
|
2023
|
+
this.connected = false;
|
|
2024
|
+
this.localMemberId = null;
|
|
2025
|
+
for (const subscription of this.subscriptions.splice(0)) {
|
|
2026
|
+
subscription.unsubscribe();
|
|
2027
|
+
}
|
|
2028
|
+
for (const peer of this.peers.values()) {
|
|
2029
|
+
this.destroyPeer(peer);
|
|
2030
|
+
}
|
|
2031
|
+
this.peers.clear();
|
|
2032
|
+
this.remoteTrackKinds.clear();
|
|
2033
|
+
this.emittedRemoteTracks.clear();
|
|
2034
|
+
this.pendingRemoteTracks.clear();
|
|
2035
|
+
}
|
|
2036
|
+
destroyPeer(peer) {
|
|
2037
|
+
peer.pc.onicecandidate = null;
|
|
2038
|
+
peer.pc.onnegotiationneeded = null;
|
|
2039
|
+
peer.pc.ontrack = null;
|
|
2040
|
+
try {
|
|
2041
|
+
peer.pc.close();
|
|
2042
|
+
} catch {
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
async createUserMediaTrack(kind, constraints) {
|
|
2046
|
+
const devices = await this.resolveMediaDevices();
|
|
2047
|
+
if (!devices?.getUserMedia || constraints === false) {
|
|
2048
|
+
return null;
|
|
2049
|
+
}
|
|
2050
|
+
const stream = await devices.getUserMedia(
|
|
2051
|
+
kind === "audio" ? { audio: constraints, video: false } : { audio: false, video: constraints }
|
|
2052
|
+
);
|
|
2053
|
+
return kind === "audio" ? stream.getAudioTracks()[0] ?? null : stream.getVideoTracks()[0] ?? null;
|
|
2054
|
+
}
|
|
2055
|
+
rememberLocalTrack(kind, track, deviceId, stopOnCleanup) {
|
|
2056
|
+
this.releaseLocalTrack(kind);
|
|
2057
|
+
this.localTracks.set(kind, {
|
|
2058
|
+
kind,
|
|
2059
|
+
track,
|
|
2060
|
+
deviceId,
|
|
2061
|
+
stopOnCleanup
|
|
2062
|
+
});
|
|
2063
|
+
}
|
|
2064
|
+
releaseLocalTrack(kind) {
|
|
2065
|
+
const local = this.localTracks.get(kind);
|
|
2066
|
+
if (!local) return;
|
|
2067
|
+
if (local.stopOnCleanup) {
|
|
2068
|
+
local.track.stop();
|
|
2069
|
+
}
|
|
2070
|
+
this.localTracks.delete(kind);
|
|
2071
|
+
}
|
|
2072
|
+
async ensureConnectedMemberId() {
|
|
2073
|
+
if (this.localMemberId) {
|
|
2074
|
+
return this.localMemberId;
|
|
2075
|
+
}
|
|
2076
|
+
return this.connect();
|
|
2077
|
+
}
|
|
2078
|
+
normalizeDescription(payload) {
|
|
2079
|
+
if (!payload || typeof payload !== "object") {
|
|
2080
|
+
return null;
|
|
2081
|
+
}
|
|
2082
|
+
const raw = payload.description;
|
|
2083
|
+
if (!raw || typeof raw.type !== "string") {
|
|
2084
|
+
return null;
|
|
2085
|
+
}
|
|
2086
|
+
return {
|
|
2087
|
+
type: raw.type,
|
|
2088
|
+
sdp: typeof raw.sdp === "string" ? raw.sdp : void 0
|
|
2089
|
+
};
|
|
2090
|
+
}
|
|
2091
|
+
normalizeCandidate(payload) {
|
|
2092
|
+
if (!payload || typeof payload !== "object") {
|
|
2093
|
+
return null;
|
|
2094
|
+
}
|
|
2095
|
+
const raw = payload.candidate;
|
|
2096
|
+
if (!raw || typeof raw.candidate !== "string") {
|
|
2097
|
+
return null;
|
|
2098
|
+
}
|
|
2099
|
+
return raw;
|
|
2100
|
+
}
|
|
2101
|
+
async ensureRuntime() {
|
|
2102
|
+
this.runtimePromise ??= import('@cloudflare/react-native-webrtc').then((mod) => {
|
|
2103
|
+
const runtime = mod;
|
|
2104
|
+
runtime.registerGlobals?.();
|
|
2105
|
+
return runtime;
|
|
2106
|
+
}).catch((error) => {
|
|
2107
|
+
throw new Error(`${installMessage2("@cloudflare/react-native-webrtc")}
|
|
2108
|
+
${String(error)}`);
|
|
2109
|
+
});
|
|
2110
|
+
return this.runtimePromise;
|
|
2111
|
+
}
|
|
2112
|
+
async resolveMediaDevices() {
|
|
2113
|
+
if (this.options.mediaDevices) {
|
|
2114
|
+
return this.options.mediaDevices;
|
|
2115
|
+
}
|
|
2116
|
+
const runtime = await this.ensureRuntime();
|
|
2117
|
+
return runtime.mediaDevices ?? (typeof navigator !== "undefined" ? navigator.mediaDevices : void 0);
|
|
2118
|
+
}
|
|
2119
|
+
get offerEvent() {
|
|
2120
|
+
return `${this.options.signalPrefix}.offer`;
|
|
2121
|
+
}
|
|
2122
|
+
get answerEvent() {
|
|
2123
|
+
return `${this.options.signalPrefix}.answer`;
|
|
2124
|
+
}
|
|
2125
|
+
get iceEvent() {
|
|
2126
|
+
return `${this.options.signalPrefix}.ice`;
|
|
2127
|
+
}
|
|
2128
|
+
};
|
|
2129
|
+
|
|
2130
|
+
// src/room.ts
|
|
1025
2131
|
var UNSAFE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
1026
2132
|
function deepSet(obj, path, value) {
|
|
1027
2133
|
const parts = path.split(".");
|
|
@@ -1147,6 +2253,21 @@ var RoomClient = class _RoomClient {
|
|
|
1147
2253
|
};
|
|
1148
2254
|
members = {
|
|
1149
2255
|
list: () => cloneValue(this._members),
|
|
2256
|
+
current: () => {
|
|
2257
|
+
const connectionId = this.currentConnectionId;
|
|
2258
|
+
if (connectionId) {
|
|
2259
|
+
const byConnection = this._members.find((member2) => member2.connectionId === connectionId);
|
|
2260
|
+
if (byConnection) {
|
|
2261
|
+
return cloneValue(byConnection);
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
const userId = this.currentUserId;
|
|
2265
|
+
if (!userId) {
|
|
2266
|
+
return null;
|
|
2267
|
+
}
|
|
2268
|
+
const member = this._members.find((entry) => entry.userId === userId) ?? null;
|
|
2269
|
+
return member ? cloneValue(member) : null;
|
|
2270
|
+
},
|
|
1150
2271
|
onSync: (handler) => this.onMembersSync(handler),
|
|
1151
2272
|
onJoin: (handler) => this.onMemberJoin(handler),
|
|
1152
2273
|
onLeave: (handler) => this.onMemberLeave(handler),
|
|
@@ -1181,6 +2302,18 @@ var RoomClient = class _RoomClient {
|
|
|
1181
2302
|
devices: {
|
|
1182
2303
|
switch: (payload) => this.switchMediaDevices(payload)
|
|
1183
2304
|
},
|
|
2305
|
+
cloudflareRealtimeKit: {
|
|
2306
|
+
createSession: (payload) => this.requestCloudflareRealtimeKitMedia("session", "POST", payload)
|
|
2307
|
+
},
|
|
2308
|
+
transport: (options) => {
|
|
2309
|
+
const provider = options?.provider ?? "cloudflare_realtimekit";
|
|
2310
|
+
if (provider === "p2p") {
|
|
2311
|
+
const p2pOptions = options?.p2p;
|
|
2312
|
+
return new RoomP2PMediaTransport(this, p2pOptions);
|
|
2313
|
+
}
|
|
2314
|
+
const cloudflareOptions = options?.cloudflareRealtimeKit;
|
|
2315
|
+
return new RoomCloudflareMediaTransport(this, cloudflareOptions);
|
|
2316
|
+
},
|
|
1184
2317
|
onTrack: (handler) => this.onMediaTrack(handler),
|
|
1185
2318
|
onTrackRemoved: (handler) => this.onMediaTrackRemoved(handler),
|
|
1186
2319
|
onStateChange: (handler) => this.onMediaStateChange(handler),
|
|
@@ -1201,7 +2334,8 @@ var RoomClient = class _RoomClient {
|
|
|
1201
2334
|
autoReconnect: options?.autoReconnect ?? true,
|
|
1202
2335
|
maxReconnectAttempts: options?.maxReconnectAttempts ?? 10,
|
|
1203
2336
|
reconnectBaseDelay: options?.reconnectBaseDelay ?? 1e3,
|
|
1204
|
-
sendTimeout: options?.sendTimeout ?? 1e4
|
|
2337
|
+
sendTimeout: options?.sendTimeout ?? 1e4,
|
|
2338
|
+
connectionTimeout: options?.connectionTimeout ?? 15e3
|
|
1205
2339
|
};
|
|
1206
2340
|
this.unsubAuthState = this.tokenManager.onAuthStateChange((user) => {
|
|
1207
2341
|
this.handleAuthStateChange(user);
|
|
@@ -1224,6 +2358,36 @@ var RoomClient = class _RoomClient {
|
|
|
1224
2358
|
async getMetadata() {
|
|
1225
2359
|
return _RoomClient.getMetadata(this.baseUrl, this.namespace, this.roomId);
|
|
1226
2360
|
}
|
|
2361
|
+
async requestCloudflareRealtimeKitMedia(path, method, payload) {
|
|
2362
|
+
return this.requestRoomMedia("cloudflare_realtimekit", path, method, payload);
|
|
2363
|
+
}
|
|
2364
|
+
async requestRoomMedia(providerPath, path, method, payload) {
|
|
2365
|
+
const token = await this.tokenManager.getAccessToken(
|
|
2366
|
+
(refreshToken) => refreshAccessToken(this.baseUrl, refreshToken)
|
|
2367
|
+
);
|
|
2368
|
+
if (!token) {
|
|
2369
|
+
throw new core.EdgeBaseError(401, "Authentication required");
|
|
2370
|
+
}
|
|
2371
|
+
const url = new URL(`${this.baseUrl.replace(/\/$/, "")}/api/room/media/${providerPath}/${path}`);
|
|
2372
|
+
url.searchParams.set("namespace", this.namespace);
|
|
2373
|
+
url.searchParams.set("id", this.roomId);
|
|
2374
|
+
const response = await fetch(url.toString(), {
|
|
2375
|
+
method,
|
|
2376
|
+
headers: {
|
|
2377
|
+
Authorization: `Bearer ${token}`,
|
|
2378
|
+
"Content-Type": "application/json"
|
|
2379
|
+
},
|
|
2380
|
+
body: method === "GET" ? void 0 : JSON.stringify(payload ?? {})
|
|
2381
|
+
});
|
|
2382
|
+
const data = await response.json().catch(() => ({}));
|
|
2383
|
+
if (!response.ok) {
|
|
2384
|
+
throw new core.EdgeBaseError(
|
|
2385
|
+
response.status,
|
|
2386
|
+
typeof data.message === "string" && data.message || `Room media request failed: ${response.statusText}`
|
|
2387
|
+
);
|
|
2388
|
+
}
|
|
2389
|
+
return data;
|
|
2390
|
+
}
|
|
1227
2391
|
/**
|
|
1228
2392
|
* Static: Get room metadata without creating a RoomClient instance.
|
|
1229
2393
|
* Useful for lobby screens where you need room info before joining.
|
|
@@ -1283,6 +2447,30 @@ var RoomClient = class _RoomClient {
|
|
|
1283
2447
|
this.reconnectInfo = null;
|
|
1284
2448
|
this.setConnectionState("disconnected");
|
|
1285
2449
|
}
|
|
2450
|
+
/** Destroy the room client, cleaning up all listeners and the auth subscription. */
|
|
2451
|
+
destroy() {
|
|
2452
|
+
this.leave();
|
|
2453
|
+
this.unsubAuthState?.();
|
|
2454
|
+
this.unsubAuthState = null;
|
|
2455
|
+
this.sharedStateHandlers.length = 0;
|
|
2456
|
+
this.playerStateHandlers.length = 0;
|
|
2457
|
+
this.messageHandlers.clear();
|
|
2458
|
+
this.allMessageHandlers.length = 0;
|
|
2459
|
+
this.errorHandlers.length = 0;
|
|
2460
|
+
this.kickedHandlers.length = 0;
|
|
2461
|
+
this.memberSyncHandlers.length = 0;
|
|
2462
|
+
this.memberJoinHandlers.length = 0;
|
|
2463
|
+
this.memberLeaveHandlers.length = 0;
|
|
2464
|
+
this.memberStateHandlers.length = 0;
|
|
2465
|
+
this.signalHandlers.clear();
|
|
2466
|
+
this.anySignalHandlers.length = 0;
|
|
2467
|
+
this.mediaTrackHandlers.length = 0;
|
|
2468
|
+
this.mediaTrackRemovedHandlers.length = 0;
|
|
2469
|
+
this.mediaStateHandlers.length = 0;
|
|
2470
|
+
this.mediaDeviceHandlers.length = 0;
|
|
2471
|
+
this.reconnectHandlers.length = 0;
|
|
2472
|
+
this.connectionStateHandlers.length = 0;
|
|
2473
|
+
}
|
|
1286
2474
|
// ─── Actions ───
|
|
1287
2475
|
/**
|
|
1288
2476
|
* Send an action to the server.
|
|
@@ -1315,31 +2503,27 @@ var RoomClient = class _RoomClient {
|
|
|
1315
2503
|
* Subscribe to shared state changes.
|
|
1316
2504
|
* Called on full sync and on each shared_delta.
|
|
1317
2505
|
*
|
|
1318
|
-
* @returns Subscription
|
|
2506
|
+
* @returns Subscription (callable & .unsubscribe())
|
|
1319
2507
|
*/
|
|
1320
2508
|
onSharedState(handler) {
|
|
1321
2509
|
this.sharedStateHandlers.push(handler);
|
|
1322
|
-
return {
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
}
|
|
1327
|
-
};
|
|
2510
|
+
return core.createSubscription(() => {
|
|
2511
|
+
const idx = this.sharedStateHandlers.indexOf(handler);
|
|
2512
|
+
if (idx >= 0) this.sharedStateHandlers.splice(idx, 1);
|
|
2513
|
+
});
|
|
1328
2514
|
}
|
|
1329
2515
|
/**
|
|
1330
2516
|
* Subscribe to player state changes.
|
|
1331
2517
|
* Called on full sync and on each player_delta.
|
|
1332
2518
|
*
|
|
1333
|
-
* @returns Subscription
|
|
2519
|
+
* @returns Subscription (callable & .unsubscribe())
|
|
1334
2520
|
*/
|
|
1335
2521
|
onPlayerState(handler) {
|
|
1336
2522
|
this.playerStateHandlers.push(handler);
|
|
1337
|
-
return {
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
}
|
|
1342
|
-
};
|
|
2523
|
+
return core.createSubscription(() => {
|
|
2524
|
+
const idx = this.playerStateHandlers.indexOf(handler);
|
|
2525
|
+
if (idx >= 0) this.playerStateHandlers.splice(idx, 1);
|
|
2526
|
+
});
|
|
1343
2527
|
}
|
|
1344
2528
|
/**
|
|
1345
2529
|
* Subscribe to messages of a specific type sent by room.sendMessage().
|
|
@@ -1347,169 +2531,137 @@ var RoomClient = class _RoomClient {
|
|
|
1347
2531
|
* @example
|
|
1348
2532
|
* room.onMessage('game_over', (data) => { console.log(data.winner); });
|
|
1349
2533
|
*
|
|
1350
|
-
* @returns Subscription
|
|
2534
|
+
* @returns Subscription (callable & .unsubscribe())
|
|
1351
2535
|
*/
|
|
1352
2536
|
onMessage(messageType, handler) {
|
|
1353
2537
|
if (!this.messageHandlers.has(messageType)) {
|
|
1354
2538
|
this.messageHandlers.set(messageType, []);
|
|
1355
2539
|
}
|
|
1356
2540
|
this.messageHandlers.get(messageType).push(handler);
|
|
1357
|
-
return {
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
if (idx >= 0) handlers.splice(idx, 1);
|
|
1363
|
-
}
|
|
2541
|
+
return core.createSubscription(() => {
|
|
2542
|
+
const handlers = this.messageHandlers.get(messageType);
|
|
2543
|
+
if (handlers) {
|
|
2544
|
+
const idx = handlers.indexOf(handler);
|
|
2545
|
+
if (idx >= 0) handlers.splice(idx, 1);
|
|
1364
2546
|
}
|
|
1365
|
-
};
|
|
2547
|
+
});
|
|
1366
2548
|
}
|
|
1367
2549
|
/**
|
|
1368
2550
|
* Subscribe to ALL messages regardless of type.
|
|
1369
2551
|
*
|
|
1370
|
-
* @returns Subscription
|
|
2552
|
+
* @returns Subscription (callable & .unsubscribe())
|
|
1371
2553
|
*/
|
|
1372
2554
|
onAnyMessage(handler) {
|
|
1373
2555
|
this.allMessageHandlers.push(handler);
|
|
1374
|
-
return {
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
}
|
|
1379
|
-
};
|
|
2556
|
+
return core.createSubscription(() => {
|
|
2557
|
+
const idx = this.allMessageHandlers.indexOf(handler);
|
|
2558
|
+
if (idx >= 0) this.allMessageHandlers.splice(idx, 1);
|
|
2559
|
+
});
|
|
1380
2560
|
}
|
|
1381
2561
|
/** Subscribe to errors */
|
|
1382
2562
|
onError(handler) {
|
|
1383
2563
|
this.errorHandlers.push(handler);
|
|
1384
|
-
return {
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
}
|
|
1389
|
-
};
|
|
2564
|
+
return core.createSubscription(() => {
|
|
2565
|
+
const idx = this.errorHandlers.indexOf(handler);
|
|
2566
|
+
if (idx >= 0) this.errorHandlers.splice(idx, 1);
|
|
2567
|
+
});
|
|
1390
2568
|
}
|
|
1391
2569
|
/** Subscribe to kick events */
|
|
1392
2570
|
onKicked(handler) {
|
|
1393
2571
|
this.kickedHandlers.push(handler);
|
|
1394
|
-
return {
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
}
|
|
1399
|
-
};
|
|
2572
|
+
return core.createSubscription(() => {
|
|
2573
|
+
const idx = this.kickedHandlers.indexOf(handler);
|
|
2574
|
+
if (idx >= 0) this.kickedHandlers.splice(idx, 1);
|
|
2575
|
+
});
|
|
1400
2576
|
}
|
|
1401
2577
|
onSignal(event, handler) {
|
|
1402
2578
|
if (!this.signalHandlers.has(event)) {
|
|
1403
2579
|
this.signalHandlers.set(event, []);
|
|
1404
2580
|
}
|
|
1405
2581
|
this.signalHandlers.get(event).push(handler);
|
|
1406
|
-
return {
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
}
|
|
1413
|
-
};
|
|
2582
|
+
return core.createSubscription(() => {
|
|
2583
|
+
const handlers = this.signalHandlers.get(event);
|
|
2584
|
+
if (!handlers) return;
|
|
2585
|
+
const index = handlers.indexOf(handler);
|
|
2586
|
+
if (index >= 0) handlers.splice(index, 1);
|
|
2587
|
+
});
|
|
1414
2588
|
}
|
|
1415
2589
|
onAnySignal(handler) {
|
|
1416
2590
|
this.anySignalHandlers.push(handler);
|
|
1417
|
-
return {
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
}
|
|
1422
|
-
};
|
|
2591
|
+
return core.createSubscription(() => {
|
|
2592
|
+
const index = this.anySignalHandlers.indexOf(handler);
|
|
2593
|
+
if (index >= 0) this.anySignalHandlers.splice(index, 1);
|
|
2594
|
+
});
|
|
1423
2595
|
}
|
|
1424
2596
|
onMembersSync(handler) {
|
|
1425
2597
|
this.memberSyncHandlers.push(handler);
|
|
1426
|
-
return {
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
}
|
|
1431
|
-
};
|
|
2598
|
+
return core.createSubscription(() => {
|
|
2599
|
+
const index = this.memberSyncHandlers.indexOf(handler);
|
|
2600
|
+
if (index >= 0) this.memberSyncHandlers.splice(index, 1);
|
|
2601
|
+
});
|
|
1432
2602
|
}
|
|
1433
2603
|
onMemberJoin(handler) {
|
|
1434
2604
|
this.memberJoinHandlers.push(handler);
|
|
1435
|
-
return {
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
}
|
|
1440
|
-
};
|
|
2605
|
+
return core.createSubscription(() => {
|
|
2606
|
+
const index = this.memberJoinHandlers.indexOf(handler);
|
|
2607
|
+
if (index >= 0) this.memberJoinHandlers.splice(index, 1);
|
|
2608
|
+
});
|
|
1441
2609
|
}
|
|
1442
2610
|
onMemberLeave(handler) {
|
|
1443
2611
|
this.memberLeaveHandlers.push(handler);
|
|
1444
|
-
return {
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
}
|
|
1449
|
-
};
|
|
2612
|
+
return core.createSubscription(() => {
|
|
2613
|
+
const index = this.memberLeaveHandlers.indexOf(handler);
|
|
2614
|
+
if (index >= 0) this.memberLeaveHandlers.splice(index, 1);
|
|
2615
|
+
});
|
|
1450
2616
|
}
|
|
1451
2617
|
onMemberStateChange(handler) {
|
|
1452
2618
|
this.memberStateHandlers.push(handler);
|
|
1453
|
-
return {
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
}
|
|
1458
|
-
};
|
|
2619
|
+
return core.createSubscription(() => {
|
|
2620
|
+
const index = this.memberStateHandlers.indexOf(handler);
|
|
2621
|
+
if (index >= 0) this.memberStateHandlers.splice(index, 1);
|
|
2622
|
+
});
|
|
1459
2623
|
}
|
|
1460
2624
|
onReconnect(handler) {
|
|
1461
2625
|
this.reconnectHandlers.push(handler);
|
|
1462
|
-
return {
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
}
|
|
1467
|
-
};
|
|
2626
|
+
return core.createSubscription(() => {
|
|
2627
|
+
const index = this.reconnectHandlers.indexOf(handler);
|
|
2628
|
+
if (index >= 0) this.reconnectHandlers.splice(index, 1);
|
|
2629
|
+
});
|
|
1468
2630
|
}
|
|
1469
2631
|
onConnectionStateChange(handler) {
|
|
1470
2632
|
this.connectionStateHandlers.push(handler);
|
|
1471
|
-
return {
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
}
|
|
1476
|
-
};
|
|
2633
|
+
return core.createSubscription(() => {
|
|
2634
|
+
const index = this.connectionStateHandlers.indexOf(handler);
|
|
2635
|
+
if (index >= 0) this.connectionStateHandlers.splice(index, 1);
|
|
2636
|
+
});
|
|
1477
2637
|
}
|
|
1478
2638
|
onMediaTrack(handler) {
|
|
1479
2639
|
this.mediaTrackHandlers.push(handler);
|
|
1480
|
-
return {
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
}
|
|
1485
|
-
};
|
|
2640
|
+
return core.createSubscription(() => {
|
|
2641
|
+
const index = this.mediaTrackHandlers.indexOf(handler);
|
|
2642
|
+
if (index >= 0) this.mediaTrackHandlers.splice(index, 1);
|
|
2643
|
+
});
|
|
1486
2644
|
}
|
|
1487
2645
|
onMediaTrackRemoved(handler) {
|
|
1488
2646
|
this.mediaTrackRemovedHandlers.push(handler);
|
|
1489
|
-
return {
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
}
|
|
1494
|
-
};
|
|
2647
|
+
return core.createSubscription(() => {
|
|
2648
|
+
const index = this.mediaTrackRemovedHandlers.indexOf(handler);
|
|
2649
|
+
if (index >= 0) this.mediaTrackRemovedHandlers.splice(index, 1);
|
|
2650
|
+
});
|
|
1495
2651
|
}
|
|
1496
2652
|
onMediaStateChange(handler) {
|
|
1497
2653
|
this.mediaStateHandlers.push(handler);
|
|
1498
|
-
return {
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
}
|
|
1503
|
-
};
|
|
2654
|
+
return core.createSubscription(() => {
|
|
2655
|
+
const index = this.mediaStateHandlers.indexOf(handler);
|
|
2656
|
+
if (index >= 0) this.mediaStateHandlers.splice(index, 1);
|
|
2657
|
+
});
|
|
1504
2658
|
}
|
|
1505
2659
|
onMediaDeviceChange(handler) {
|
|
1506
2660
|
this.mediaDeviceHandlers.push(handler);
|
|
1507
|
-
return {
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
}
|
|
1512
|
-
};
|
|
2661
|
+
return core.createSubscription(() => {
|
|
2662
|
+
const index = this.mediaDeviceHandlers.indexOf(handler);
|
|
2663
|
+
if (index >= 0) this.mediaDeviceHandlers.splice(index, 1);
|
|
2664
|
+
});
|
|
1513
2665
|
}
|
|
1514
2666
|
async sendSignal(event, payload, options) {
|
|
1515
2667
|
if (!this.ws || !this.connected || !this.authenticated) {
|
|
@@ -1616,22 +2768,42 @@ var RoomClient = class _RoomClient {
|
|
|
1616
2768
|
const wsUrl = this.buildWsUrl();
|
|
1617
2769
|
const ws = new WebSocket(wsUrl);
|
|
1618
2770
|
this.ws = ws;
|
|
2771
|
+
let settled = false;
|
|
2772
|
+
const connectionTimer = setTimeout(() => {
|
|
2773
|
+
if (!settled) {
|
|
2774
|
+
settled = true;
|
|
2775
|
+
try {
|
|
2776
|
+
ws.close();
|
|
2777
|
+
} catch (_) {
|
|
2778
|
+
}
|
|
2779
|
+
this.ws = null;
|
|
2780
|
+
reject(new core.EdgeBaseError(408, `Room WebSocket connection timed out after ${this.options.connectionTimeout}ms. Is the server running?`));
|
|
2781
|
+
}
|
|
2782
|
+
}, this.options.connectionTimeout);
|
|
1619
2783
|
ws.onopen = () => {
|
|
2784
|
+
clearTimeout(connectionTimer);
|
|
1620
2785
|
this.connected = true;
|
|
1621
2786
|
this.reconnectAttempts = 0;
|
|
1622
2787
|
this.startHeartbeat();
|
|
1623
2788
|
this.authenticate().then(() => {
|
|
1624
|
-
|
|
1625
|
-
|
|
2789
|
+
if (!settled) {
|
|
2790
|
+
settled = true;
|
|
2791
|
+
this.waitingForAuth = false;
|
|
2792
|
+
resolve();
|
|
2793
|
+
}
|
|
1626
2794
|
}).catch((error) => {
|
|
1627
|
-
|
|
1628
|
-
|
|
2795
|
+
if (!settled) {
|
|
2796
|
+
settled = true;
|
|
2797
|
+
this.handleAuthenticationFailure(error);
|
|
2798
|
+
reject(error);
|
|
2799
|
+
}
|
|
1629
2800
|
});
|
|
1630
2801
|
};
|
|
1631
2802
|
ws.onmessage = (event) => {
|
|
1632
2803
|
this.handleMessage(event.data);
|
|
1633
2804
|
};
|
|
1634
2805
|
ws.onclose = (event) => {
|
|
2806
|
+
clearTimeout(connectionTimer);
|
|
1635
2807
|
this.connected = false;
|
|
1636
2808
|
this.authenticated = false;
|
|
1637
2809
|
this.joined = false;
|
|
@@ -1640,6 +2812,9 @@ var RoomClient = class _RoomClient {
|
|
|
1640
2812
|
if (event.code === 4004 && this.connectionState !== "kicked") {
|
|
1641
2813
|
this.handleKicked();
|
|
1642
2814
|
}
|
|
2815
|
+
if (!this.intentionallyLeft) {
|
|
2816
|
+
this.rejectAllPendingRequests(new core.EdgeBaseError(499, "WebSocket connection lost"));
|
|
2817
|
+
}
|
|
1643
2818
|
if (!this.intentionallyLeft && !this.waitingForAuth && this.options.autoReconnect && this.reconnectAttempts < this.options.maxReconnectAttempts) {
|
|
1644
2819
|
this.scheduleReconnect();
|
|
1645
2820
|
} else if (!this.intentionallyLeft && this.connectionState !== "kicked" && this.connectionState !== "auth_lost") {
|
|
@@ -1647,7 +2822,11 @@ var RoomClient = class _RoomClient {
|
|
|
1647
2822
|
}
|
|
1648
2823
|
};
|
|
1649
2824
|
ws.onerror = () => {
|
|
1650
|
-
|
|
2825
|
+
clearTimeout(connectionTimer);
|
|
2826
|
+
if (!settled) {
|
|
2827
|
+
settled = true;
|
|
2828
|
+
reject(new core.EdgeBaseError(500, "Room WebSocket connection error"));
|
|
2829
|
+
}
|
|
1651
2830
|
};
|
|
1652
2831
|
});
|
|
1653
2832
|
}
|
|
@@ -2091,6 +3270,9 @@ var RoomClient = class _RoomClient {
|
|
|
2091
3270
|
this.sendRaw({ type: "auth", token });
|
|
2092
3271
|
}
|
|
2093
3272
|
handleAuthStateChange(user) {
|
|
3273
|
+
if (user === null) {
|
|
3274
|
+
this.rejectAllPendingRequests(new core.EdgeBaseError(401, "Auth state lost"));
|
|
3275
|
+
}
|
|
2094
3276
|
if (user) {
|
|
2095
3277
|
if (this.ws && this.connected && this.authenticated) {
|
|
2096
3278
|
this.refreshAuth();
|
|
@@ -2345,6 +3527,17 @@ var RoomClient = class _RoomClient {
|
|
|
2345
3527
|
[kind]: next
|
|
2346
3528
|
};
|
|
2347
3529
|
}
|
|
3530
|
+
rejectAllPendingRequests(error) {
|
|
3531
|
+
for (const [, pending] of this.pendingRequests) {
|
|
3532
|
+
clearTimeout(pending.timeout);
|
|
3533
|
+
pending.reject(error);
|
|
3534
|
+
}
|
|
3535
|
+
this.pendingRequests.clear();
|
|
3536
|
+
this.rejectPendingVoidRequests(this.pendingSignalRequests, error);
|
|
3537
|
+
this.rejectPendingVoidRequests(this.pendingAdminRequests, error);
|
|
3538
|
+
this.rejectPendingVoidRequests(this.pendingMemberStateRequests, error);
|
|
3539
|
+
this.rejectPendingVoidRequests(this.pendingMediaRequests, error);
|
|
3540
|
+
}
|
|
2348
3541
|
rejectPendingVoidRequests(pendingRequests, error) {
|
|
2349
3542
|
for (const [, pending] of pendingRequests) {
|
|
2350
3543
|
clearTimeout(pending.timeout);
|
|
@@ -2375,7 +3568,9 @@ var RoomClient = class _RoomClient {
|
|
|
2375
3568
|
}
|
|
2376
3569
|
scheduleReconnect() {
|
|
2377
3570
|
const attempt = this.reconnectAttempts + 1;
|
|
2378
|
-
const
|
|
3571
|
+
const baseDelay = this.options.reconnectBaseDelay * Math.pow(2, this.reconnectAttempts);
|
|
3572
|
+
const jitter = Math.random() * baseDelay * 0.25;
|
|
3573
|
+
const delay = baseDelay + jitter;
|
|
2379
3574
|
this.reconnectAttempts++;
|
|
2380
3575
|
this.reconnectInfo = { attempt };
|
|
2381
3576
|
this.setConnectionState("reconnecting");
|