@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.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EdgeBaseError, ApiPaths, ContextManager, HttpClient, DefaultDbApi, HttpClientAdapter, PublicHttpClientAdapter, StorageClient, FunctionsClient, DbRef } from '@edge-base/core';
|
|
1
|
+
import { EdgeBaseError, createSubscription, 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, {
|
|
@@ -646,7 +646,7 @@ var DatabaseLiveClient = class {
|
|
|
646
646
|
}
|
|
647
647
|
this.connect(channel).catch(() => {
|
|
648
648
|
});
|
|
649
|
-
return () => {
|
|
649
|
+
return createSubscription(() => {
|
|
650
650
|
const subs = this.subscriptions.get(channel);
|
|
651
651
|
if (!subs) return;
|
|
652
652
|
const idx = subs.indexOf(sub);
|
|
@@ -657,14 +657,14 @@ var DatabaseLiveClient = class {
|
|
|
657
657
|
this.channelOrFilters.delete(channel);
|
|
658
658
|
this.sendUnsubscribe(channel);
|
|
659
659
|
}
|
|
660
|
-
};
|
|
660
|
+
});
|
|
661
661
|
}
|
|
662
662
|
onError(handler) {
|
|
663
663
|
this.errorHandlers.push(handler);
|
|
664
|
-
return () => {
|
|
664
|
+
return createSubscription(() => {
|
|
665
665
|
const idx = this.errorHandlers.indexOf(handler);
|
|
666
666
|
if (idx >= 0) this.errorHandlers.splice(idx, 1);
|
|
667
|
-
};
|
|
667
|
+
});
|
|
668
668
|
}
|
|
669
669
|
async connect(channel) {
|
|
670
670
|
this.connectedChannels.add(channel);
|
|
@@ -752,7 +752,7 @@ var DatabaseLiveClient = class {
|
|
|
752
752
|
(refreshToken) => refreshAccessToken(this.baseUrl, refreshToken)
|
|
753
753
|
);
|
|
754
754
|
if (!token) throw new EdgeBaseError(401, "No access token available. Sign in first.");
|
|
755
|
-
this.sendRaw({ type: "auth", token, sdkVersion: "0.1
|
|
755
|
+
this.sendRaw({ type: "auth", token, sdkVersion: "0.2.1" });
|
|
756
756
|
return new Promise((resolve, reject) => {
|
|
757
757
|
const timeout = setTimeout(() => reject(new EdgeBaseError(401, "Auth timeout")), 1e4);
|
|
758
758
|
const original = this.ws?.onmessage;
|
|
@@ -872,7 +872,7 @@ var DatabaseLiveClient = class {
|
|
|
872
872
|
refreshAuth() {
|
|
873
873
|
const token = this.tokenManager.currentAccessToken;
|
|
874
874
|
if (!token || !this.ws || !this.connected) return;
|
|
875
|
-
this.sendRaw({ type: "auth", token, sdkVersion: "0.1
|
|
875
|
+
this.sendRaw({ type: "auth", token, sdkVersion: "0.2.1" });
|
|
876
876
|
}
|
|
877
877
|
handleAuthStateChange(user) {
|
|
878
878
|
if (user) {
|
|
@@ -935,7 +935,9 @@ var DatabaseLiveClient = class {
|
|
|
935
935
|
}
|
|
936
936
|
}
|
|
937
937
|
scheduleReconnect(channel) {
|
|
938
|
-
const
|
|
938
|
+
const baseDelay = this.options.reconnectBaseDelay * Math.pow(2, this.reconnectAttempts);
|
|
939
|
+
const jitter = Math.random() * baseDelay * 0.25;
|
|
940
|
+
const delay = baseDelay + jitter;
|
|
939
941
|
this.reconnectAttempts++;
|
|
940
942
|
setTimeout(() => {
|
|
941
943
|
this.connect(channel).catch(() => {
|
|
@@ -1011,11 +1013,1115 @@ function matchesDatabaseLiveChannel(channel, change, messageChannel) {
|
|
|
1011
1013
|
if (parts.length === 2) return parts[1] === change.table;
|
|
1012
1014
|
if (parts.length === 3) return parts[2] === change.table;
|
|
1013
1015
|
if (parts.length === 4) {
|
|
1014
|
-
if (parts[2] === change.table
|
|
1016
|
+
if (parts[2] === change.table && change.docId === parts[3]) return true;
|
|
1015
1017
|
return parts[3] === change.table;
|
|
1016
1018
|
}
|
|
1017
1019
|
return parts[3] === change.table && change.docId === parts[4];
|
|
1018
1020
|
}
|
|
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
|
|
1019
2125
|
var UNSAFE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
1020
2126
|
function deepSet(obj, path, value) {
|
|
1021
2127
|
const parts = path.split(".");
|
|
@@ -1141,6 +2247,21 @@ var RoomClient = class _RoomClient {
|
|
|
1141
2247
|
};
|
|
1142
2248
|
members = {
|
|
1143
2249
|
list: () => cloneValue(this._members),
|
|
2250
|
+
current: () => {
|
|
2251
|
+
const connectionId = this.currentConnectionId;
|
|
2252
|
+
if (connectionId) {
|
|
2253
|
+
const byConnection = this._members.find((member2) => member2.connectionId === connectionId);
|
|
2254
|
+
if (byConnection) {
|
|
2255
|
+
return cloneValue(byConnection);
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
const userId = this.currentUserId;
|
|
2259
|
+
if (!userId) {
|
|
2260
|
+
return null;
|
|
2261
|
+
}
|
|
2262
|
+
const member = this._members.find((entry) => entry.userId === userId) ?? null;
|
|
2263
|
+
return member ? cloneValue(member) : null;
|
|
2264
|
+
},
|
|
1144
2265
|
onSync: (handler) => this.onMembersSync(handler),
|
|
1145
2266
|
onJoin: (handler) => this.onMemberJoin(handler),
|
|
1146
2267
|
onLeave: (handler) => this.onMemberLeave(handler),
|
|
@@ -1175,6 +2296,18 @@ var RoomClient = class _RoomClient {
|
|
|
1175
2296
|
devices: {
|
|
1176
2297
|
switch: (payload) => this.switchMediaDevices(payload)
|
|
1177
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
|
+
},
|
|
1178
2311
|
onTrack: (handler) => this.onMediaTrack(handler),
|
|
1179
2312
|
onTrackRemoved: (handler) => this.onMediaTrackRemoved(handler),
|
|
1180
2313
|
onStateChange: (handler) => this.onMediaStateChange(handler),
|
|
@@ -1195,7 +2328,8 @@ var RoomClient = class _RoomClient {
|
|
|
1195
2328
|
autoReconnect: options?.autoReconnect ?? true,
|
|
1196
2329
|
maxReconnectAttempts: options?.maxReconnectAttempts ?? 10,
|
|
1197
2330
|
reconnectBaseDelay: options?.reconnectBaseDelay ?? 1e3,
|
|
1198
|
-
sendTimeout: options?.sendTimeout ?? 1e4
|
|
2331
|
+
sendTimeout: options?.sendTimeout ?? 1e4,
|
|
2332
|
+
connectionTimeout: options?.connectionTimeout ?? 15e3
|
|
1199
2333
|
};
|
|
1200
2334
|
this.unsubAuthState = this.tokenManager.onAuthStateChange((user) => {
|
|
1201
2335
|
this.handleAuthStateChange(user);
|
|
@@ -1218,6 +2352,36 @@ var RoomClient = class _RoomClient {
|
|
|
1218
2352
|
async getMetadata() {
|
|
1219
2353
|
return _RoomClient.getMetadata(this.baseUrl, this.namespace, this.roomId);
|
|
1220
2354
|
}
|
|
2355
|
+
async requestCloudflareRealtimeKitMedia(path, method, payload) {
|
|
2356
|
+
return this.requestRoomMedia("cloudflare_realtimekit", path, method, payload);
|
|
2357
|
+
}
|
|
2358
|
+
async requestRoomMedia(providerPath, path, method, payload) {
|
|
2359
|
+
const token = await this.tokenManager.getAccessToken(
|
|
2360
|
+
(refreshToken) => refreshAccessToken(this.baseUrl, refreshToken)
|
|
2361
|
+
);
|
|
2362
|
+
if (!token) {
|
|
2363
|
+
throw new EdgeBaseError(401, "Authentication required");
|
|
2364
|
+
}
|
|
2365
|
+
const url = new URL(`${this.baseUrl.replace(/\/$/, "")}/api/room/media/${providerPath}/${path}`);
|
|
2366
|
+
url.searchParams.set("namespace", this.namespace);
|
|
2367
|
+
url.searchParams.set("id", this.roomId);
|
|
2368
|
+
const response = await fetch(url.toString(), {
|
|
2369
|
+
method,
|
|
2370
|
+
headers: {
|
|
2371
|
+
Authorization: `Bearer ${token}`,
|
|
2372
|
+
"Content-Type": "application/json"
|
|
2373
|
+
},
|
|
2374
|
+
body: method === "GET" ? void 0 : JSON.stringify(payload ?? {})
|
|
2375
|
+
});
|
|
2376
|
+
const data = await response.json().catch(() => ({}));
|
|
2377
|
+
if (!response.ok) {
|
|
2378
|
+
throw new EdgeBaseError(
|
|
2379
|
+
response.status,
|
|
2380
|
+
typeof data.message === "string" && data.message || `Room media request failed: ${response.statusText}`
|
|
2381
|
+
);
|
|
2382
|
+
}
|
|
2383
|
+
return data;
|
|
2384
|
+
}
|
|
1221
2385
|
/**
|
|
1222
2386
|
* Static: Get room metadata without creating a RoomClient instance.
|
|
1223
2387
|
* Useful for lobby screens where you need room info before joining.
|
|
@@ -1277,6 +2441,30 @@ var RoomClient = class _RoomClient {
|
|
|
1277
2441
|
this.reconnectInfo = null;
|
|
1278
2442
|
this.setConnectionState("disconnected");
|
|
1279
2443
|
}
|
|
2444
|
+
/** Destroy the room client, cleaning up all listeners and the auth subscription. */
|
|
2445
|
+
destroy() {
|
|
2446
|
+
this.leave();
|
|
2447
|
+
this.unsubAuthState?.();
|
|
2448
|
+
this.unsubAuthState = null;
|
|
2449
|
+
this.sharedStateHandlers.length = 0;
|
|
2450
|
+
this.playerStateHandlers.length = 0;
|
|
2451
|
+
this.messageHandlers.clear();
|
|
2452
|
+
this.allMessageHandlers.length = 0;
|
|
2453
|
+
this.errorHandlers.length = 0;
|
|
2454
|
+
this.kickedHandlers.length = 0;
|
|
2455
|
+
this.memberSyncHandlers.length = 0;
|
|
2456
|
+
this.memberJoinHandlers.length = 0;
|
|
2457
|
+
this.memberLeaveHandlers.length = 0;
|
|
2458
|
+
this.memberStateHandlers.length = 0;
|
|
2459
|
+
this.signalHandlers.clear();
|
|
2460
|
+
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
|
+
this.reconnectHandlers.length = 0;
|
|
2466
|
+
this.connectionStateHandlers.length = 0;
|
|
2467
|
+
}
|
|
1280
2468
|
// ─── Actions ───
|
|
1281
2469
|
/**
|
|
1282
2470
|
* Send an action to the server.
|
|
@@ -1309,31 +2497,27 @@ var RoomClient = class _RoomClient {
|
|
|
1309
2497
|
* Subscribe to shared state changes.
|
|
1310
2498
|
* Called on full sync and on each shared_delta.
|
|
1311
2499
|
*
|
|
1312
|
-
* @returns Subscription
|
|
2500
|
+
* @returns Subscription (callable & .unsubscribe())
|
|
1313
2501
|
*/
|
|
1314
2502
|
onSharedState(handler) {
|
|
1315
2503
|
this.sharedStateHandlers.push(handler);
|
|
1316
|
-
return {
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
}
|
|
1321
|
-
};
|
|
2504
|
+
return createSubscription(() => {
|
|
2505
|
+
const idx = this.sharedStateHandlers.indexOf(handler);
|
|
2506
|
+
if (idx >= 0) this.sharedStateHandlers.splice(idx, 1);
|
|
2507
|
+
});
|
|
1322
2508
|
}
|
|
1323
2509
|
/**
|
|
1324
2510
|
* Subscribe to player state changes.
|
|
1325
2511
|
* Called on full sync and on each player_delta.
|
|
1326
2512
|
*
|
|
1327
|
-
* @returns Subscription
|
|
2513
|
+
* @returns Subscription (callable & .unsubscribe())
|
|
1328
2514
|
*/
|
|
1329
2515
|
onPlayerState(handler) {
|
|
1330
2516
|
this.playerStateHandlers.push(handler);
|
|
1331
|
-
return {
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
}
|
|
1336
|
-
};
|
|
2517
|
+
return createSubscription(() => {
|
|
2518
|
+
const idx = this.playerStateHandlers.indexOf(handler);
|
|
2519
|
+
if (idx >= 0) this.playerStateHandlers.splice(idx, 1);
|
|
2520
|
+
});
|
|
1337
2521
|
}
|
|
1338
2522
|
/**
|
|
1339
2523
|
* Subscribe to messages of a specific type sent by room.sendMessage().
|
|
@@ -1341,169 +2525,137 @@ var RoomClient = class _RoomClient {
|
|
|
1341
2525
|
* @example
|
|
1342
2526
|
* room.onMessage('game_over', (data) => { console.log(data.winner); });
|
|
1343
2527
|
*
|
|
1344
|
-
* @returns Subscription
|
|
2528
|
+
* @returns Subscription (callable & .unsubscribe())
|
|
1345
2529
|
*/
|
|
1346
2530
|
onMessage(messageType, handler) {
|
|
1347
2531
|
if (!this.messageHandlers.has(messageType)) {
|
|
1348
2532
|
this.messageHandlers.set(messageType, []);
|
|
1349
2533
|
}
|
|
1350
2534
|
this.messageHandlers.get(messageType).push(handler);
|
|
1351
|
-
return {
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
if (idx >= 0) handlers.splice(idx, 1);
|
|
1357
|
-
}
|
|
2535
|
+
return createSubscription(() => {
|
|
2536
|
+
const handlers = this.messageHandlers.get(messageType);
|
|
2537
|
+
if (handlers) {
|
|
2538
|
+
const idx = handlers.indexOf(handler);
|
|
2539
|
+
if (idx >= 0) handlers.splice(idx, 1);
|
|
1358
2540
|
}
|
|
1359
|
-
};
|
|
2541
|
+
});
|
|
1360
2542
|
}
|
|
1361
2543
|
/**
|
|
1362
2544
|
* Subscribe to ALL messages regardless of type.
|
|
1363
2545
|
*
|
|
1364
|
-
* @returns Subscription
|
|
2546
|
+
* @returns Subscription (callable & .unsubscribe())
|
|
1365
2547
|
*/
|
|
1366
2548
|
onAnyMessage(handler) {
|
|
1367
2549
|
this.allMessageHandlers.push(handler);
|
|
1368
|
-
return {
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
}
|
|
1373
|
-
};
|
|
2550
|
+
return createSubscription(() => {
|
|
2551
|
+
const idx = this.allMessageHandlers.indexOf(handler);
|
|
2552
|
+
if (idx >= 0) this.allMessageHandlers.splice(idx, 1);
|
|
2553
|
+
});
|
|
1374
2554
|
}
|
|
1375
2555
|
/** Subscribe to errors */
|
|
1376
2556
|
onError(handler) {
|
|
1377
2557
|
this.errorHandlers.push(handler);
|
|
1378
|
-
return {
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
}
|
|
1383
|
-
};
|
|
2558
|
+
return createSubscription(() => {
|
|
2559
|
+
const idx = this.errorHandlers.indexOf(handler);
|
|
2560
|
+
if (idx >= 0) this.errorHandlers.splice(idx, 1);
|
|
2561
|
+
});
|
|
1384
2562
|
}
|
|
1385
2563
|
/** Subscribe to kick events */
|
|
1386
2564
|
onKicked(handler) {
|
|
1387
2565
|
this.kickedHandlers.push(handler);
|
|
1388
|
-
return {
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
}
|
|
1393
|
-
};
|
|
2566
|
+
return createSubscription(() => {
|
|
2567
|
+
const idx = this.kickedHandlers.indexOf(handler);
|
|
2568
|
+
if (idx >= 0) this.kickedHandlers.splice(idx, 1);
|
|
2569
|
+
});
|
|
1394
2570
|
}
|
|
1395
2571
|
onSignal(event, handler) {
|
|
1396
2572
|
if (!this.signalHandlers.has(event)) {
|
|
1397
2573
|
this.signalHandlers.set(event, []);
|
|
1398
2574
|
}
|
|
1399
2575
|
this.signalHandlers.get(event).push(handler);
|
|
1400
|
-
return {
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
}
|
|
1407
|
-
};
|
|
2576
|
+
return createSubscription(() => {
|
|
2577
|
+
const handlers = this.signalHandlers.get(event);
|
|
2578
|
+
if (!handlers) return;
|
|
2579
|
+
const index = handlers.indexOf(handler);
|
|
2580
|
+
if (index >= 0) handlers.splice(index, 1);
|
|
2581
|
+
});
|
|
1408
2582
|
}
|
|
1409
2583
|
onAnySignal(handler) {
|
|
1410
2584
|
this.anySignalHandlers.push(handler);
|
|
1411
|
-
return {
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
}
|
|
1416
|
-
};
|
|
2585
|
+
return createSubscription(() => {
|
|
2586
|
+
const index = this.anySignalHandlers.indexOf(handler);
|
|
2587
|
+
if (index >= 0) this.anySignalHandlers.splice(index, 1);
|
|
2588
|
+
});
|
|
1417
2589
|
}
|
|
1418
2590
|
onMembersSync(handler) {
|
|
1419
2591
|
this.memberSyncHandlers.push(handler);
|
|
1420
|
-
return {
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
}
|
|
1425
|
-
};
|
|
2592
|
+
return createSubscription(() => {
|
|
2593
|
+
const index = this.memberSyncHandlers.indexOf(handler);
|
|
2594
|
+
if (index >= 0) this.memberSyncHandlers.splice(index, 1);
|
|
2595
|
+
});
|
|
1426
2596
|
}
|
|
1427
2597
|
onMemberJoin(handler) {
|
|
1428
2598
|
this.memberJoinHandlers.push(handler);
|
|
1429
|
-
return {
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
}
|
|
1434
|
-
};
|
|
2599
|
+
return createSubscription(() => {
|
|
2600
|
+
const index = this.memberJoinHandlers.indexOf(handler);
|
|
2601
|
+
if (index >= 0) this.memberJoinHandlers.splice(index, 1);
|
|
2602
|
+
});
|
|
1435
2603
|
}
|
|
1436
2604
|
onMemberLeave(handler) {
|
|
1437
2605
|
this.memberLeaveHandlers.push(handler);
|
|
1438
|
-
return {
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
}
|
|
1443
|
-
};
|
|
2606
|
+
return createSubscription(() => {
|
|
2607
|
+
const index = this.memberLeaveHandlers.indexOf(handler);
|
|
2608
|
+
if (index >= 0) this.memberLeaveHandlers.splice(index, 1);
|
|
2609
|
+
});
|
|
1444
2610
|
}
|
|
1445
2611
|
onMemberStateChange(handler) {
|
|
1446
2612
|
this.memberStateHandlers.push(handler);
|
|
1447
|
-
return {
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
}
|
|
1452
|
-
};
|
|
2613
|
+
return createSubscription(() => {
|
|
2614
|
+
const index = this.memberStateHandlers.indexOf(handler);
|
|
2615
|
+
if (index >= 0) this.memberStateHandlers.splice(index, 1);
|
|
2616
|
+
});
|
|
1453
2617
|
}
|
|
1454
2618
|
onReconnect(handler) {
|
|
1455
2619
|
this.reconnectHandlers.push(handler);
|
|
1456
|
-
return {
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
}
|
|
1461
|
-
};
|
|
2620
|
+
return createSubscription(() => {
|
|
2621
|
+
const index = this.reconnectHandlers.indexOf(handler);
|
|
2622
|
+
if (index >= 0) this.reconnectHandlers.splice(index, 1);
|
|
2623
|
+
});
|
|
1462
2624
|
}
|
|
1463
2625
|
onConnectionStateChange(handler) {
|
|
1464
2626
|
this.connectionStateHandlers.push(handler);
|
|
1465
|
-
return {
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
}
|
|
1470
|
-
};
|
|
2627
|
+
return createSubscription(() => {
|
|
2628
|
+
const index = this.connectionStateHandlers.indexOf(handler);
|
|
2629
|
+
if (index >= 0) this.connectionStateHandlers.splice(index, 1);
|
|
2630
|
+
});
|
|
1471
2631
|
}
|
|
1472
2632
|
onMediaTrack(handler) {
|
|
1473
2633
|
this.mediaTrackHandlers.push(handler);
|
|
1474
|
-
return {
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
}
|
|
1479
|
-
};
|
|
2634
|
+
return createSubscription(() => {
|
|
2635
|
+
const index = this.mediaTrackHandlers.indexOf(handler);
|
|
2636
|
+
if (index >= 0) this.mediaTrackHandlers.splice(index, 1);
|
|
2637
|
+
});
|
|
1480
2638
|
}
|
|
1481
2639
|
onMediaTrackRemoved(handler) {
|
|
1482
2640
|
this.mediaTrackRemovedHandlers.push(handler);
|
|
1483
|
-
return {
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
}
|
|
1488
|
-
};
|
|
2641
|
+
return createSubscription(() => {
|
|
2642
|
+
const index = this.mediaTrackRemovedHandlers.indexOf(handler);
|
|
2643
|
+
if (index >= 0) this.mediaTrackRemovedHandlers.splice(index, 1);
|
|
2644
|
+
});
|
|
1489
2645
|
}
|
|
1490
2646
|
onMediaStateChange(handler) {
|
|
1491
2647
|
this.mediaStateHandlers.push(handler);
|
|
1492
|
-
return {
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
}
|
|
1497
|
-
};
|
|
2648
|
+
return createSubscription(() => {
|
|
2649
|
+
const index = this.mediaStateHandlers.indexOf(handler);
|
|
2650
|
+
if (index >= 0) this.mediaStateHandlers.splice(index, 1);
|
|
2651
|
+
});
|
|
1498
2652
|
}
|
|
1499
2653
|
onMediaDeviceChange(handler) {
|
|
1500
2654
|
this.mediaDeviceHandlers.push(handler);
|
|
1501
|
-
return {
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
}
|
|
1506
|
-
};
|
|
2655
|
+
return createSubscription(() => {
|
|
2656
|
+
const index = this.mediaDeviceHandlers.indexOf(handler);
|
|
2657
|
+
if (index >= 0) this.mediaDeviceHandlers.splice(index, 1);
|
|
2658
|
+
});
|
|
1507
2659
|
}
|
|
1508
2660
|
async sendSignal(event, payload, options) {
|
|
1509
2661
|
if (!this.ws || !this.connected || !this.authenticated) {
|
|
@@ -1610,22 +2762,42 @@ var RoomClient = class _RoomClient {
|
|
|
1610
2762
|
const wsUrl = this.buildWsUrl();
|
|
1611
2763
|
const ws = new WebSocket(wsUrl);
|
|
1612
2764
|
this.ws = ws;
|
|
2765
|
+
let settled = false;
|
|
2766
|
+
const connectionTimer = setTimeout(() => {
|
|
2767
|
+
if (!settled) {
|
|
2768
|
+
settled = true;
|
|
2769
|
+
try {
|
|
2770
|
+
ws.close();
|
|
2771
|
+
} catch (_) {
|
|
2772
|
+
}
|
|
2773
|
+
this.ws = null;
|
|
2774
|
+
reject(new EdgeBaseError(408, `Room WebSocket connection timed out after ${this.options.connectionTimeout}ms. Is the server running?`));
|
|
2775
|
+
}
|
|
2776
|
+
}, this.options.connectionTimeout);
|
|
1613
2777
|
ws.onopen = () => {
|
|
2778
|
+
clearTimeout(connectionTimer);
|
|
1614
2779
|
this.connected = true;
|
|
1615
2780
|
this.reconnectAttempts = 0;
|
|
1616
2781
|
this.startHeartbeat();
|
|
1617
2782
|
this.authenticate().then(() => {
|
|
1618
|
-
|
|
1619
|
-
|
|
2783
|
+
if (!settled) {
|
|
2784
|
+
settled = true;
|
|
2785
|
+
this.waitingForAuth = false;
|
|
2786
|
+
resolve();
|
|
2787
|
+
}
|
|
1620
2788
|
}).catch((error) => {
|
|
1621
|
-
|
|
1622
|
-
|
|
2789
|
+
if (!settled) {
|
|
2790
|
+
settled = true;
|
|
2791
|
+
this.handleAuthenticationFailure(error);
|
|
2792
|
+
reject(error);
|
|
2793
|
+
}
|
|
1623
2794
|
});
|
|
1624
2795
|
};
|
|
1625
2796
|
ws.onmessage = (event) => {
|
|
1626
2797
|
this.handleMessage(event.data);
|
|
1627
2798
|
};
|
|
1628
2799
|
ws.onclose = (event) => {
|
|
2800
|
+
clearTimeout(connectionTimer);
|
|
1629
2801
|
this.connected = false;
|
|
1630
2802
|
this.authenticated = false;
|
|
1631
2803
|
this.joined = false;
|
|
@@ -1634,6 +2806,9 @@ var RoomClient = class _RoomClient {
|
|
|
1634
2806
|
if (event.code === 4004 && this.connectionState !== "kicked") {
|
|
1635
2807
|
this.handleKicked();
|
|
1636
2808
|
}
|
|
2809
|
+
if (!this.intentionallyLeft) {
|
|
2810
|
+
this.rejectAllPendingRequests(new EdgeBaseError(499, "WebSocket connection lost"));
|
|
2811
|
+
}
|
|
1637
2812
|
if (!this.intentionallyLeft && !this.waitingForAuth && this.options.autoReconnect && this.reconnectAttempts < this.options.maxReconnectAttempts) {
|
|
1638
2813
|
this.scheduleReconnect();
|
|
1639
2814
|
} else if (!this.intentionallyLeft && this.connectionState !== "kicked" && this.connectionState !== "auth_lost") {
|
|
@@ -1641,7 +2816,11 @@ var RoomClient = class _RoomClient {
|
|
|
1641
2816
|
}
|
|
1642
2817
|
};
|
|
1643
2818
|
ws.onerror = () => {
|
|
1644
|
-
|
|
2819
|
+
clearTimeout(connectionTimer);
|
|
2820
|
+
if (!settled) {
|
|
2821
|
+
settled = true;
|
|
2822
|
+
reject(new EdgeBaseError(500, "Room WebSocket connection error"));
|
|
2823
|
+
}
|
|
1645
2824
|
};
|
|
1646
2825
|
});
|
|
1647
2826
|
}
|
|
@@ -2085,6 +3264,9 @@ var RoomClient = class _RoomClient {
|
|
|
2085
3264
|
this.sendRaw({ type: "auth", token });
|
|
2086
3265
|
}
|
|
2087
3266
|
handleAuthStateChange(user) {
|
|
3267
|
+
if (user === null) {
|
|
3268
|
+
this.rejectAllPendingRequests(new EdgeBaseError(401, "Auth state lost"));
|
|
3269
|
+
}
|
|
2088
3270
|
if (user) {
|
|
2089
3271
|
if (this.ws && this.connected && this.authenticated) {
|
|
2090
3272
|
this.refreshAuth();
|
|
@@ -2339,6 +3521,17 @@ var RoomClient = class _RoomClient {
|
|
|
2339
3521
|
[kind]: next
|
|
2340
3522
|
};
|
|
2341
3523
|
}
|
|
3524
|
+
rejectAllPendingRequests(error) {
|
|
3525
|
+
for (const [, pending] of this.pendingRequests) {
|
|
3526
|
+
clearTimeout(pending.timeout);
|
|
3527
|
+
pending.reject(error);
|
|
3528
|
+
}
|
|
3529
|
+
this.pendingRequests.clear();
|
|
3530
|
+
this.rejectPendingVoidRequests(this.pendingSignalRequests, error);
|
|
3531
|
+
this.rejectPendingVoidRequests(this.pendingAdminRequests, error);
|
|
3532
|
+
this.rejectPendingVoidRequests(this.pendingMemberStateRequests, error);
|
|
3533
|
+
this.rejectPendingVoidRequests(this.pendingMediaRequests, error);
|
|
3534
|
+
}
|
|
2342
3535
|
rejectPendingVoidRequests(pendingRequests, error) {
|
|
2343
3536
|
for (const [, pending] of pendingRequests) {
|
|
2344
3537
|
clearTimeout(pending.timeout);
|
|
@@ -2369,7 +3562,9 @@ var RoomClient = class _RoomClient {
|
|
|
2369
3562
|
}
|
|
2370
3563
|
scheduleReconnect() {
|
|
2371
3564
|
const attempt = this.reconnectAttempts + 1;
|
|
2372
|
-
const
|
|
3565
|
+
const baseDelay = this.options.reconnectBaseDelay * Math.pow(2, this.reconnectAttempts);
|
|
3566
|
+
const jitter = Math.random() * baseDelay * 0.25;
|
|
3567
|
+
const delay = baseDelay + jitter;
|
|
2373
3568
|
this.reconnectAttempts++;
|
|
2374
3569
|
this.reconnectInfo = { attempt };
|
|
2375
3570
|
this.setConnectionState("reconnecting");
|