@xmaxai/web-sdk 1.0.3 → 1.0.4
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 +468 -0
- package/dist/{chunk-JWAAS6P4.cjs → chunk-FEL5MJMJ.cjs} +1876 -294
- package/dist/chunk-FEL5MJMJ.cjs.map +1 -0
- package/dist/{chunk-YMLRUDAO.js → chunk-FMAQXPTE.js} +1790 -208
- package/dist/chunk-FMAQXPTE.js.map +1 -0
- package/dist/index.cjs +4 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +57 -13
- package/dist/index.d.ts +57 -13
- package/dist/index.js +3 -1
- package/dist/react/index.cjs +3 -3
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.js +1 -1
- package/package.json +3 -1
- package/dist/chunk-JWAAS6P4.cjs.map +0 -1
- package/dist/chunk-YMLRUDAO.js.map +0 -1
|
@@ -3,10 +3,10 @@ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { en
|
|
|
3
3
|
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
4
|
|
|
5
5
|
// src/Foundation/Runtime/XmaxEnvironment.ts
|
|
6
|
-
var XmaxEnvironment = /* @__PURE__ */ ((
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
return
|
|
6
|
+
var XmaxEnvironment = /* @__PURE__ */ ((XmaxEnvironment3) => {
|
|
7
|
+
XmaxEnvironment3["china"] = "china";
|
|
8
|
+
XmaxEnvironment3["global"] = "global";
|
|
9
|
+
return XmaxEnvironment3;
|
|
10
10
|
})(XmaxEnvironment || {});
|
|
11
11
|
var API_BASE_URLS = {
|
|
12
12
|
["china" /* china */]: "https://cloud.xmax.22duck.cn/open/api/v1",
|
|
@@ -198,7 +198,7 @@ var XmaxError = class _XmaxError extends Error {
|
|
|
198
198
|
};
|
|
199
199
|
|
|
200
200
|
// src/Foundation/Runtime/RuntimeInfo.ts
|
|
201
|
-
var XMAX_SDK_VERSION = "1.0.
|
|
201
|
+
var XMAX_SDK_VERSION = "1.0.4";
|
|
202
202
|
var _RuntimeInfo = class _RuntimeInfo {
|
|
203
203
|
/**
|
|
204
204
|
* 序列化为接口传输使用的 snake_case 字段。
|
|
@@ -521,22 +521,22 @@ var RealtimeVideoFormat = class _RealtimeVideoFormat {
|
|
|
521
521
|
* @throws 尺寸、帧率或码率配置无效时抛出错误。
|
|
522
522
|
*/
|
|
523
523
|
validate() {
|
|
524
|
-
if (this.width <= 0 || this.height <= 0 || this.fps <= 0 || this.width % 2 !== 0 || this.height % 2 !== 0) {
|
|
524
|
+
if (this.width <= 0 || this.height <= 0 || !Number.isFinite(this.fps) || this.fps <= 0 || this.width % 2 !== 0 || this.height % 2 !== 0) {
|
|
525
525
|
throw new XmaxError(
|
|
526
526
|
"INVALID_CONFIGURATION" /* invalidConfiguration */,
|
|
527
527
|
"Realtime video width and height must be positive even numbers, and fps must be greater than zero"
|
|
528
528
|
);
|
|
529
529
|
}
|
|
530
|
-
if (this.minimumBitrate !== void 0 && this.minimumBitrate < 0) {
|
|
530
|
+
if (this.minimumBitrate !== void 0 && (!Number.isFinite(this.minimumBitrate) || this.minimumBitrate < 0)) {
|
|
531
531
|
throw new XmaxError(
|
|
532
532
|
"INVALID_CONFIGURATION" /* invalidConfiguration */,
|
|
533
|
-
"Minimum bitrate must not
|
|
533
|
+
"Minimum bitrate must be finite and not negative"
|
|
534
534
|
);
|
|
535
535
|
}
|
|
536
|
-
if (this.maximumBitrate !== void 0 && this.maximumBitrate <= 0) {
|
|
536
|
+
if (this.maximumBitrate !== void 0 && (!Number.isFinite(this.maximumBitrate) || this.maximumBitrate <= 0)) {
|
|
537
537
|
throw new XmaxError(
|
|
538
538
|
"INVALID_CONFIGURATION" /* invalidConfiguration */,
|
|
539
|
-
"Maximum bitrate must be greater than zero"
|
|
539
|
+
"Maximum bitrate must be finite and greater than zero"
|
|
540
540
|
);
|
|
541
541
|
}
|
|
542
542
|
if (this.minimumBitrate !== void 0 && this.maximumBitrate !== void 0 && this.minimumBitrate > this.maximumBitrate) {
|
|
@@ -564,26 +564,39 @@ var RealtimeVideoFormat = class _RealtimeVideoFormat {
|
|
|
564
564
|
// src/Service/Realtime/RealtimeModel.ts
|
|
565
565
|
var RealtimeModel = /* @__PURE__ */ ((RealtimeModel2) => {
|
|
566
566
|
RealtimeModel2["x2_0"] = "x2.0";
|
|
567
|
-
RealtimeModel2["x2_0_pro"] = "x2.0-pro";
|
|
568
567
|
RealtimeModel2["x2_0_trtc"] = "x2.0-trtc";
|
|
568
|
+
RealtimeModel2["x2_0_agora"] = "x2.0-agora";
|
|
569
|
+
RealtimeModel2["x2_1_preview"] = "x2.1-preview";
|
|
569
570
|
return RealtimeModel2;
|
|
570
571
|
})(RealtimeModel || {});
|
|
572
|
+
var MODEL_BASE_URLS = {
|
|
573
|
+
["x2.0-agora" /* x2_0_agora */]: "https://dev.xmaxai.com/open/api/v1",
|
|
574
|
+
["x2.1-preview" /* x2_1_preview */]: "https://dev.xmaxai.com/open/api/v1"
|
|
575
|
+
};
|
|
571
576
|
var RESOLUTION_BUCKETS = {
|
|
572
|
-
["x2.
|
|
573
|
-
|
|
577
|
+
["x2.1-preview" /* x2_1_preview */]: [
|
|
578
|
+
{ width: 1024, height: 1920 },
|
|
579
|
+
{ width: 1920, height: 1024 }
|
|
580
|
+
],
|
|
581
|
+
["x2.0-agora" /* x2_0_agora */]: [
|
|
574
582
|
{ width: 1024, height: 1920 },
|
|
575
583
|
{ width: 1920, height: 1024 }
|
|
576
584
|
],
|
|
585
|
+
["x2.0" /* x2_0 */]: [],
|
|
577
586
|
["x2.0-trtc" /* x2_0_trtc */]: [
|
|
578
587
|
{ width: 1024, height: 1920 },
|
|
579
588
|
{ width: 1920, height: 1024 }
|
|
580
589
|
]
|
|
581
590
|
};
|
|
582
591
|
var MAXIMUM_INPUT_PIXELS = {
|
|
592
|
+
["x2.1-preview" /* x2_1_preview */]: 21e5,
|
|
593
|
+
["x2.0-agora" /* x2_0_agora */]: 21e5,
|
|
583
594
|
["x2.0" /* x2_0 */]: 128e4,
|
|
584
|
-
["x2.0-pro" /* x2_0_pro */]: 21e5,
|
|
585
595
|
["x2.0-trtc" /* x2_0_trtc */]: 21e5
|
|
586
596
|
};
|
|
597
|
+
function modelBaseURL(model) {
|
|
598
|
+
return MODEL_BASE_URLS[model];
|
|
599
|
+
}
|
|
587
600
|
function resolutionBuckets(model) {
|
|
588
601
|
return RESOLUTION_BUCKETS[model];
|
|
589
602
|
}
|
|
@@ -1071,7 +1084,718 @@ async function watchMediaPermissionPrompt(includeMicrophone) {
|
|
|
1071
1084
|
return { decided, dispose };
|
|
1072
1085
|
}
|
|
1073
1086
|
|
|
1074
|
-
// src/Foundation/RTC/
|
|
1087
|
+
// src/Foundation/RTC/Agora/AgoraRtcStatistics.ts
|
|
1088
|
+
var AgoraRtcStatistics = class _AgoraRtcStatistics {
|
|
1089
|
+
/**
|
|
1090
|
+
* 采集已发布的视频和已订阅的结果流统计,性能日志不控制回调采集。
|
|
1091
|
+
*/
|
|
1092
|
+
static collect(client, listener) {
|
|
1093
|
+
const rtc = client.getRTCStats();
|
|
1094
|
+
const local = client.getLocalVideoStats();
|
|
1095
|
+
const valid = _AgoraRtcStatistics.valid;
|
|
1096
|
+
const loss = _AgoraRtcStatistics.loss;
|
|
1097
|
+
const localVideo = client.localTracks.some((track) => track.trackMediaType === "video") ? {
|
|
1098
|
+
width: valid(local.sendResolutionWidth, 1),
|
|
1099
|
+
height: valid(local.sendResolutionHeight, 1),
|
|
1100
|
+
frameRate: valid(local.sendFrameRate),
|
|
1101
|
+
bitrateKbps: valid(local.sendBitrate / 1e3),
|
|
1102
|
+
uplinkLossPercent: loss(local.currentPacketLossRate)
|
|
1103
|
+
} : void 0;
|
|
1104
|
+
const remote = Object.entries(client.getRemoteVideoStats()).map(([userID, stats]) => ({
|
|
1105
|
+
userID,
|
|
1106
|
+
width: valid(stats.receiveResolutionWidth, 1),
|
|
1107
|
+
height: valid(stats.receiveResolutionHeight, 1),
|
|
1108
|
+
frameRate: valid(stats.receiveFrameRate),
|
|
1109
|
+
bitrateKbps: valid(stats.receiveBitrate / 1e3),
|
|
1110
|
+
rttMs: valid(rtc.RTT),
|
|
1111
|
+
uplinkLossPercent: localVideo?.uplinkLossPercent,
|
|
1112
|
+
downlinkLossPercent: loss(stats.currentPacketLossRate),
|
|
1113
|
+
endToEndDelayMs: valid(stats.end2EndDelay)
|
|
1114
|
+
}));
|
|
1115
|
+
listener?.onLocalVideoStatistics?.(localVideo && Object.freeze(localVideo));
|
|
1116
|
+
listener?.onRemoteVideoStatistics?.(Object.freeze(remote.map((item) => Object.freeze(item))));
|
|
1117
|
+
XmaxLogger.rtc.info(() => `Agora \u8FD0\u884C\u7EDF\u8BA1 (Agora RTC Statistics)
|
|
1118
|
+
${JSON.stringify({
|
|
1119
|
+
rttMs: valid(rtc.RTT),
|
|
1120
|
+
sentBytes: valid(rtc.SendBytes),
|
|
1121
|
+
receivedBytes: valid(rtc.RecvBytes),
|
|
1122
|
+
localVideo,
|
|
1123
|
+
remoteVideo: remote,
|
|
1124
|
+
localAudio: client.getLocalAudioStats(),
|
|
1125
|
+
remoteAudio: client.getRemoteAudioStats()
|
|
1126
|
+
}, null, 2)}`, 2 /* performance */);
|
|
1127
|
+
}
|
|
1128
|
+
/**
|
|
1129
|
+
* 网络质量等级范围与公共枚举一致;不将整体 RTT 冒充上下行独立 RTT。
|
|
1130
|
+
*/
|
|
1131
|
+
static network(stats, listener) {
|
|
1132
|
+
const quality = (value) => Number.isInteger(value) && value >= 0 && value <= 6 ? value : void 0;
|
|
1133
|
+
const result = Object.freeze({ uplinkQuality: quality(stats.uplinkNetworkQuality), downlinkQuality: quality(stats.downlinkNetworkQuality) });
|
|
1134
|
+
listener?.onNetworkStatistics?.(result);
|
|
1135
|
+
XmaxLogger.rtc.info(() => `Agora \u7F51\u7EDC\u8D28\u91CF (Agora Network Quality)
|
|
1136
|
+
${JSON.stringify(result, null, 2)}`, 2 /* performance */);
|
|
1137
|
+
}
|
|
1138
|
+
/**
|
|
1139
|
+
* 过滤缺失、非有限或不合理的统计值。
|
|
1140
|
+
*/
|
|
1141
|
+
static valid(value, minimum = 0) {
|
|
1142
|
+
return typeof value === "number" && Number.isFinite(value) && value >= minimum ? value : void 0;
|
|
1143
|
+
}
|
|
1144
|
+
/**
|
|
1145
|
+
* 声网丢包率为 0–1 比例,转换为公共百分比。
|
|
1146
|
+
*/
|
|
1147
|
+
static loss(value) {
|
|
1148
|
+
return Number.isFinite(value) && value >= 0 && value <= 1 ? value * 100 : void 0;
|
|
1149
|
+
}
|
|
1150
|
+
};
|
|
1151
|
+
|
|
1152
|
+
// src/Foundation/RTC/RtcVideoOrientation.ts
|
|
1153
|
+
function shouldTransposeRtcVideoSize(environment) {
|
|
1154
|
+
const userAgent = environment?.userAgent ?? (typeof navigator === "undefined" ? "" : navigator.userAgent);
|
|
1155
|
+
const maxTouchPoints = environment?.maxTouchPoints ?? (typeof navigator === "undefined" ? 0 : navigator.maxTouchPoints ?? 0);
|
|
1156
|
+
const mobile = /Android|webOS|iPhone|iPad|iPod|Mobile/i.test(userAgent) || /Macintosh/i.test(userAgent) && maxTouchPoints > 1;
|
|
1157
|
+
if (!mobile) {
|
|
1158
|
+
return false;
|
|
1159
|
+
}
|
|
1160
|
+
const orientationType = environment?.orientationType ?? (typeof window === "undefined" ? void 0 : window.screen?.orientation?.type);
|
|
1161
|
+
if (orientationType !== void 0) {
|
|
1162
|
+
return orientationType.includes("portrait");
|
|
1163
|
+
}
|
|
1164
|
+
const degrees = environment?.orientationDegrees ?? (typeof window === "undefined" ? void 0 : window.orientation);
|
|
1165
|
+
return degrees === 0 || degrees === 180;
|
|
1166
|
+
}
|
|
1167
|
+
function rtcOrientedVideoSize(size, environment) {
|
|
1168
|
+
return shouldTransposeRtcVideoSize(environment) ? { width: size.height, height: size.width } : size;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
// src/Foundation/RTC/Agora/AgoraRtcManager.ts
|
|
1172
|
+
var _AgoraRtcManager = class _AgoraRtcManager {
|
|
1173
|
+
/**
|
|
1174
|
+
* 保存服务环境与加载器,不触发浏览器依赖或设备权限请求。
|
|
1175
|
+
*/
|
|
1176
|
+
constructor(options) {
|
|
1177
|
+
/**
|
|
1178
|
+
* 配置与 RTC 资源
|
|
1179
|
+
*/
|
|
1180
|
+
__publicField(this, "environment");
|
|
1181
|
+
__publicField(this, "loadSDK");
|
|
1182
|
+
__publicField(this, "sdk");
|
|
1183
|
+
__publicField(this, "client");
|
|
1184
|
+
__publicField(this, "initialization");
|
|
1185
|
+
__publicField(this, "lifecycle", 0);
|
|
1186
|
+
__publicField(this, "camera");
|
|
1187
|
+
__publicField(this, "microphone");
|
|
1188
|
+
__publicField(this, "connection");
|
|
1189
|
+
__publicField(this, "roomVersion", 0);
|
|
1190
|
+
/**
|
|
1191
|
+
* 凭据恢复与发布意图;RTC 自动断开可能清空 localTracks,因此单独保留发布意图。
|
|
1192
|
+
*/
|
|
1193
|
+
__publicField(this, "tokenExpired", false);
|
|
1194
|
+
__publicField(this, "rejoinOperation");
|
|
1195
|
+
__publicField(this, "rejoinTask");
|
|
1196
|
+
__publicField(this, "cancelRejoin");
|
|
1197
|
+
__publicField(this, "videoPublishRequested", false);
|
|
1198
|
+
__publicField(this, "audioPublishRequested", false);
|
|
1199
|
+
/**
|
|
1200
|
+
* 事件、统计与音频播放状态
|
|
1201
|
+
*/
|
|
1202
|
+
__publicField(this, "listener");
|
|
1203
|
+
__publicField(this, "statisticsTimer");
|
|
1204
|
+
__publicField(this, "audioVolumes", /* @__PURE__ */ new Map());
|
|
1205
|
+
__publicField(this, "requestedAudio", /* @__PURE__ */ new Set());
|
|
1206
|
+
/**
|
|
1207
|
+
* DataStream 串行发送与限速状态;等待中的消息通过房间版本取消。
|
|
1208
|
+
*/
|
|
1209
|
+
__publicField(this, "sendTail", Promise.resolve());
|
|
1210
|
+
__publicField(this, "sentPackets", []);
|
|
1211
|
+
__publicField(this, "cancelSendDelay");
|
|
1212
|
+
__publicField(this, "cancelActiveSend");
|
|
1213
|
+
this.environment = options.environment;
|
|
1214
|
+
this.loadSDK = options.loadSDK ?? (async () => (await import("agora-rtc-sdk-ng")).default);
|
|
1215
|
+
}
|
|
1216
|
+
/**
|
|
1217
|
+
* 当前实例是否已创建声网 client。
|
|
1218
|
+
*/
|
|
1219
|
+
get isInitialized() {
|
|
1220
|
+
return this.client !== void 0;
|
|
1221
|
+
}
|
|
1222
|
+
/**
|
|
1223
|
+
* 声网音频轨支持播放前设置音量;零音量额外停止播放以防浏览器差异。
|
|
1224
|
+
*/
|
|
1225
|
+
get supportsRemoteAudioVolumeControl() {
|
|
1226
|
+
return true;
|
|
1227
|
+
}
|
|
1228
|
+
/**
|
|
1229
|
+
* 共享本实例的初始化任务,区域完全由接入方环境决定。
|
|
1230
|
+
*/
|
|
1231
|
+
async initialize() {
|
|
1232
|
+
if (this.client) return;
|
|
1233
|
+
if (this.initialization) return this.initialization;
|
|
1234
|
+
const version = this.lifecycle;
|
|
1235
|
+
const operation = this.performInitialize(version);
|
|
1236
|
+
this.initialization = operation;
|
|
1237
|
+
try {
|
|
1238
|
+
await operation;
|
|
1239
|
+
} finally {
|
|
1240
|
+
if (this.initialization === operation) this.initialization = void 0;
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
/**
|
|
1244
|
+
* 加载模块、锁定区域并注册事件;迟到的初始化不能复活已销毁实例。
|
|
1245
|
+
*/
|
|
1246
|
+
async performInitialize(version) {
|
|
1247
|
+
const sdk = await this.loadSDK();
|
|
1248
|
+
if (version !== this.lifecycle) throw this.cancelled();
|
|
1249
|
+
const region = _AgoraRtcManager.regions.get(sdk);
|
|
1250
|
+
if (region && region.environment !== this.environment) {
|
|
1251
|
+
throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Concurrent Agora clients must use the same XmaxEnvironment");
|
|
1252
|
+
}
|
|
1253
|
+
sdk.setLogLevel(4);
|
|
1254
|
+
if (!region) sdk.setArea([this.environment === "china" /* china */ ? "CHINA" : "GLOBAL"]);
|
|
1255
|
+
const client = sdk.createClient({ mode: "rtc", codec: "h264" });
|
|
1256
|
+
if (typeof client.sendStreamMessage !== "function") {
|
|
1257
|
+
throw new XmaxError("RTC_ERROR" /* rtcError */, "Agora DataStream is unavailable in this SDK version");
|
|
1258
|
+
}
|
|
1259
|
+
_AgoraRtcManager.regions.set(sdk, { environment: this.environment, count: (region?.count ?? 0) + 1 });
|
|
1260
|
+
this.sdk = sdk;
|
|
1261
|
+
this.client = client;
|
|
1262
|
+
this.registerEvents(client);
|
|
1263
|
+
}
|
|
1264
|
+
/**
|
|
1265
|
+
* 先失效全部异步工作,再释放房间、采集、监听和区域引用。
|
|
1266
|
+
*/
|
|
1267
|
+
async destroy() {
|
|
1268
|
+
this.lifecycle++;
|
|
1269
|
+
const client = this.client;
|
|
1270
|
+
this.listener = void 0;
|
|
1271
|
+
this.clearRoom();
|
|
1272
|
+
this.client = void 0;
|
|
1273
|
+
client?.removeAllListeners();
|
|
1274
|
+
this.camera?.close();
|
|
1275
|
+
this.microphone?.close();
|
|
1276
|
+
this.camera = void 0;
|
|
1277
|
+
this.microphone = void 0;
|
|
1278
|
+
const sdk = this.sdk;
|
|
1279
|
+
this.sdk = void 0;
|
|
1280
|
+
try {
|
|
1281
|
+
await client?.leave();
|
|
1282
|
+
} catch {
|
|
1283
|
+
XmaxLogger.rtc.warning(() => "Agora \u9000\u623F\u5931\u8D25\uFF0C\u5DF2\u91CA\u653E\u672C\u5730\u8D44\u6E90 (Agora Leave Failed)");
|
|
1284
|
+
} finally {
|
|
1285
|
+
const region = sdk && _AgoraRtcManager.regions.get(sdk);
|
|
1286
|
+
if (sdk && region && --region.count === 0) _AgoraRtcManager.regions.delete(sdk);
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
/**
|
|
1290
|
+
* 通过声网采集相机但不发布,返回原生轨道供公共渲染层使用。
|
|
1291
|
+
*/
|
|
1292
|
+
async startCameraCapture(options) {
|
|
1293
|
+
const client = this.requireClient();
|
|
1294
|
+
if (this.camera) throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Camera capture is already running");
|
|
1295
|
+
try {
|
|
1296
|
+
const captureSize = rtcOrientedVideoSize(options);
|
|
1297
|
+
const camera = await this.sdk.createCameraVideoTrack({
|
|
1298
|
+
facingMode: options.position === "front" /* front */ ? "user" : "environment",
|
|
1299
|
+
encoderConfig: { width: captureSize.width, height: captureSize.height, frameRate: options.frameRate }
|
|
1300
|
+
});
|
|
1301
|
+
if (this.client !== client) {
|
|
1302
|
+
camera.close();
|
|
1303
|
+
throw this.cancelled();
|
|
1304
|
+
}
|
|
1305
|
+
this.camera = camera;
|
|
1306
|
+
return camera.getMediaStreamTrack();
|
|
1307
|
+
} catch (error) {
|
|
1308
|
+
throw this.mapError(error, "CAMERA_PERMISSION_DENIED" /* cameraPermissionDenied */);
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
/**
|
|
1312
|
+
* 切换声网相机设备并返回切换后的原生轨道。
|
|
1313
|
+
*/
|
|
1314
|
+
async switchCameraCapture(to) {
|
|
1315
|
+
const camera = this.requireCamera();
|
|
1316
|
+
await this.run(() => camera.setDevice({ facingMode: to === "front" /* front */ ? "user" : "environment" }));
|
|
1317
|
+
if (this.camera !== camera) throw this.cancelled();
|
|
1318
|
+
return camera.getMediaStreamTrack();
|
|
1319
|
+
}
|
|
1320
|
+
/**
|
|
1321
|
+
* 停止采集并释放摄像头。
|
|
1322
|
+
*/
|
|
1323
|
+
async stopCameraCapture() {
|
|
1324
|
+
const camera = this.camera;
|
|
1325
|
+
this.camera = void 0;
|
|
1326
|
+
camera?.close();
|
|
1327
|
+
}
|
|
1328
|
+
/**
|
|
1329
|
+
* 应用公共编码配置,补偿移动端竖屏的宽高转置。
|
|
1330
|
+
*/
|
|
1331
|
+
async configureVideoEncoding(config) {
|
|
1332
|
+
const camera = this.requireCamera();
|
|
1333
|
+
const encodingSize = rtcOrientedVideoSize(config);
|
|
1334
|
+
await this.run(async () => {
|
|
1335
|
+
await camera.setEncoderConfiguration({
|
|
1336
|
+
width: encodingSize.width,
|
|
1337
|
+
height: encodingSize.height,
|
|
1338
|
+
frameRate: config.frameRate,
|
|
1339
|
+
bitrateMin: config.minimumBitrate,
|
|
1340
|
+
bitrateMax: config.maximumBitrate
|
|
1341
|
+
});
|
|
1342
|
+
await camera.setOptimizationMode(config.encoderPreference === "maintainFramerate" /* maintainFramerate */ ? "motion" : "detail");
|
|
1343
|
+
});
|
|
1344
|
+
}
|
|
1345
|
+
/**
|
|
1346
|
+
* 使用会话下发的字符串频道与 UID 入房;不采信后端区域覆盖。
|
|
1347
|
+
*/
|
|
1348
|
+
async joinRoom(configuration) {
|
|
1349
|
+
await this.rejoinTask?.catch(() => {
|
|
1350
|
+
});
|
|
1351
|
+
const client = this.requireClient();
|
|
1352
|
+
if (configuration.provider !== "agora" || this.connection) {
|
|
1353
|
+
throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Agora credentials and an idle room are required");
|
|
1354
|
+
}
|
|
1355
|
+
const version = ++this.roomVersion;
|
|
1356
|
+
await this.run(() => client.join(configuration.appID, configuration.roomID, configuration.roomToken, configuration.userID));
|
|
1357
|
+
if (client !== this.client || version !== this.roomVersion) {
|
|
1358
|
+
await client.leave();
|
|
1359
|
+
throw this.cancelled();
|
|
1360
|
+
}
|
|
1361
|
+
this.connection = configuration;
|
|
1362
|
+
for (const user of client.remoteUsers) {
|
|
1363
|
+
if (user.hasVideo) this.listener?.onRemoteVideoPublished(String(user.uid), true);
|
|
1364
|
+
}
|
|
1365
|
+
this.statisticsTimer = setInterval(() => {
|
|
1366
|
+
if (this.client !== client || !this.connection || this.tokenExpired || this.rejoinOperation) return;
|
|
1367
|
+
try {
|
|
1368
|
+
AgoraRtcStatistics.collect(client, this.listener);
|
|
1369
|
+
} catch {
|
|
1370
|
+
XmaxLogger.rtc.warning(() => "Agora \u7EDF\u8BA1\u91C7\u6837\u5931\u8D25 (Agora Statistics Failed)");
|
|
1371
|
+
}
|
|
1372
|
+
}, 2e3);
|
|
1373
|
+
}
|
|
1374
|
+
/**
|
|
1375
|
+
* 退房前取消排队消息、统计与音频播放;本地相机预览保持运行。
|
|
1376
|
+
*/
|
|
1377
|
+
async leaveRoom() {
|
|
1378
|
+
this.clearRoom();
|
|
1379
|
+
if (this.client) await this.run(() => this.client.leave());
|
|
1380
|
+
}
|
|
1381
|
+
/**
|
|
1382
|
+
* 应用同一频道的新 Token;即将过期时续签,已经过期时重新进房并恢复媒体。
|
|
1383
|
+
*/
|
|
1384
|
+
async updateCredentials(config, signal) {
|
|
1385
|
+
if (signal?.aborted) throw this.cancelled();
|
|
1386
|
+
const current = this.connection;
|
|
1387
|
+
const client = this.requireClient();
|
|
1388
|
+
if (!current || config.provider !== "agora" || config.appID !== current.appID || config.roomID !== current.roomID || config.userID !== current.userID) {
|
|
1389
|
+
throw new XmaxError("SESSION_ERROR" /* sessionError */, "Agora session binding changed");
|
|
1390
|
+
}
|
|
1391
|
+
const version = this.roomVersion;
|
|
1392
|
+
if (!this.tokenExpired) {
|
|
1393
|
+
try {
|
|
1394
|
+
await this.run(() => client.renewToken(config.roomToken));
|
|
1395
|
+
} catch (error) {
|
|
1396
|
+
this.ensureRoomCurrent(client, version);
|
|
1397
|
+
if (!this.tokenExpired) throw error;
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
this.ensureRoomCurrent(client, version);
|
|
1401
|
+
if (signal?.aborted) throw this.cancelled();
|
|
1402
|
+
if (this.tokenExpired) await this.rejoinRoom(client, config, signal);
|
|
1403
|
+
else this.connection = config;
|
|
1404
|
+
}
|
|
1405
|
+
/**
|
|
1406
|
+
* 串行恢复同一 RTC 房间,不创建业务会话或重新发送生成开始指令。
|
|
1407
|
+
*/
|
|
1408
|
+
async rejoinRoom(client, config, signal) {
|
|
1409
|
+
if (this.rejoinOperation) return this.rejoinOperation;
|
|
1410
|
+
const version = ++this.roomVersion;
|
|
1411
|
+
const task = Promise.resolve().then(() => this.performRejoin(client, config, version));
|
|
1412
|
+
this.rejoinTask = task;
|
|
1413
|
+
void task.finally(() => {
|
|
1414
|
+
if (this.rejoinTask === task) this.rejoinTask = void 0;
|
|
1415
|
+
}).catch(() => {
|
|
1416
|
+
});
|
|
1417
|
+
let cancel;
|
|
1418
|
+
const operation = new Promise((resolve, reject) => {
|
|
1419
|
+
cancel = () => {
|
|
1420
|
+
if (this.isRoomCurrent(client, version)) this.roomVersion++;
|
|
1421
|
+
reject(this.cancelled());
|
|
1422
|
+
};
|
|
1423
|
+
this.cancelRejoin = cancel;
|
|
1424
|
+
signal?.addEventListener("abort", cancel, { once: true });
|
|
1425
|
+
task.then(resolve, reject);
|
|
1426
|
+
if (signal?.aborted) cancel();
|
|
1427
|
+
});
|
|
1428
|
+
this.rejoinOperation = operation;
|
|
1429
|
+
try {
|
|
1430
|
+
await operation;
|
|
1431
|
+
} finally {
|
|
1432
|
+
signal?.removeEventListener("abort", cancel);
|
|
1433
|
+
if (this.cancelRejoin === cancel) this.cancelRejoin = void 0;
|
|
1434
|
+
if (this.rejoinOperation === operation) this.rejoinOperation = void 0;
|
|
1435
|
+
}
|
|
1436
|
+
this.ensureRoomCurrent(client, version);
|
|
1437
|
+
for (const user of client.remoteUsers) {
|
|
1438
|
+
if (user.hasVideo) this.listener?.onRemoteVideoPublished(String(user.uid), true);
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
/**
|
|
1442
|
+
* 使用新 Token 重建 RTC 连接,复用采集轨道并恢复远端音频订阅。
|
|
1443
|
+
*/
|
|
1444
|
+
async performRejoin(client, config, version) {
|
|
1445
|
+
this.ensureRoomCurrent(client, version);
|
|
1446
|
+
this.cancelSendDelay?.();
|
|
1447
|
+
this.cancelActiveSend?.();
|
|
1448
|
+
this.listener?.onRoomRejoining?.();
|
|
1449
|
+
for (const user of client.remoteUsers) user.audioTrack?.stop();
|
|
1450
|
+
await this.run(() => client.leave());
|
|
1451
|
+
this.ensureRoomCurrent(client, version);
|
|
1452
|
+
await this.run(() => client.join(config.appID, config.roomID, config.roomToken, config.userID));
|
|
1453
|
+
if (!this.isRoomCurrent(client, version)) {
|
|
1454
|
+
await this.run(() => client.leave());
|
|
1455
|
+
throw this.cancelled();
|
|
1456
|
+
}
|
|
1457
|
+
if (this.videoPublishRequested && this.camera) {
|
|
1458
|
+
await this.run(() => client.publish(this.camera));
|
|
1459
|
+
this.ensureRoomCurrent(client, version);
|
|
1460
|
+
}
|
|
1461
|
+
if (this.audioPublishRequested && this.microphone) {
|
|
1462
|
+
await this.run(() => client.publish(this.microphone));
|
|
1463
|
+
this.ensureRoomCurrent(client, version);
|
|
1464
|
+
}
|
|
1465
|
+
for (const user of client.remoteUsers) {
|
|
1466
|
+
if (!user.hasAudio || !this.requestedAudio.has(String(user.uid))) continue;
|
|
1467
|
+
const track = await this.run(() => client.subscribe(user, "audio"));
|
|
1468
|
+
if (!this.isRoomCurrent(client, version)) {
|
|
1469
|
+
track.stop();
|
|
1470
|
+
throw this.cancelled();
|
|
1471
|
+
}
|
|
1472
|
+
this.applyAudioVolume(user);
|
|
1473
|
+
}
|
|
1474
|
+
this.connection = config;
|
|
1475
|
+
this.tokenExpired = false;
|
|
1476
|
+
this.sentPackets = [];
|
|
1477
|
+
}
|
|
1478
|
+
/**
|
|
1479
|
+
* 发布已采集的视频轨道。
|
|
1480
|
+
*/
|
|
1481
|
+
async publishLocalVideo() {
|
|
1482
|
+
const client = this.requireClient();
|
|
1483
|
+
const camera = this.requireCamera();
|
|
1484
|
+
this.videoPublishRequested = true;
|
|
1485
|
+
await this.rejoinOperation;
|
|
1486
|
+
if (client !== this.client || !this.videoPublishRequested) throw this.cancelled();
|
|
1487
|
+
if (client.localTracks.includes(camera)) return;
|
|
1488
|
+
await this.run(() => client.publish(camera));
|
|
1489
|
+
}
|
|
1490
|
+
/**
|
|
1491
|
+
* 停止视频发布但保留采集与预览。
|
|
1492
|
+
*/
|
|
1493
|
+
async unpublishLocalVideo() {
|
|
1494
|
+
this.videoPublishRequested = false;
|
|
1495
|
+
if (this.rejoinTask || this.tokenExpired) return;
|
|
1496
|
+
if (this.client && this.camera) await this.run(() => this.client.unpublish(this.camera));
|
|
1497
|
+
}
|
|
1498
|
+
/**
|
|
1499
|
+
* 首次发布时申请麦克风;取消后的迟到采集立即释放。
|
|
1500
|
+
*/
|
|
1501
|
+
async publishLocalAudio() {
|
|
1502
|
+
const client = this.requireClient();
|
|
1503
|
+
this.audioPublishRequested = true;
|
|
1504
|
+
await this.rejoinOperation;
|
|
1505
|
+
const version = this.roomVersion;
|
|
1506
|
+
if (client !== this.client || !this.audioPublishRequested) throw this.cancelled();
|
|
1507
|
+
try {
|
|
1508
|
+
if (!this.microphone) {
|
|
1509
|
+
const track = await this.sdk.createMicrophoneAudioTrack();
|
|
1510
|
+
if (client !== this.client || version !== this.roomVersion) {
|
|
1511
|
+
track.close();
|
|
1512
|
+
throw this.cancelled();
|
|
1513
|
+
}
|
|
1514
|
+
this.microphone = track;
|
|
1515
|
+
}
|
|
1516
|
+
if (!client.localTracks.includes(this.microphone)) await client.publish(this.microphone);
|
|
1517
|
+
} catch (error) {
|
|
1518
|
+
throw this.mapError(error, "MICROPHONE_PERMISSION_DENIED" /* microphonePermissionDenied */);
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
/**
|
|
1522
|
+
* 停止音频发布,采集交给关闭流程释放。
|
|
1523
|
+
*/
|
|
1524
|
+
async unpublishLocalAudio() {
|
|
1525
|
+
this.audioPublishRequested = false;
|
|
1526
|
+
if (this.rejoinTask || this.tokenExpired) return;
|
|
1527
|
+
if (this.client && this.microphone) await this.run(() => this.client.unpublish(this.microphone));
|
|
1528
|
+
}
|
|
1529
|
+
/**
|
|
1530
|
+
* 订阅远端主视频并返回原生轨道,不创建额外播放视图。
|
|
1531
|
+
*/
|
|
1532
|
+
async subscribeRemoteVideo(userID, subscribe) {
|
|
1533
|
+
if (!subscribe && this.rejoinTask) return;
|
|
1534
|
+
await this.rejoinOperation;
|
|
1535
|
+
const client = this.requireClient();
|
|
1536
|
+
const user = client.remoteUsers.find((user2) => String(user2.uid) === userID);
|
|
1537
|
+
if (!user && !subscribe) return;
|
|
1538
|
+
if (!user) throw new XmaxError("RTC_ERROR" /* rtcError */, "Agora remote video user is unavailable");
|
|
1539
|
+
if (!subscribe) {
|
|
1540
|
+
await this.run(() => client.unsubscribe(user, "video"));
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
1543
|
+
const version = this.roomVersion;
|
|
1544
|
+
const track = await this.run(() => client.subscribe(user, "video"));
|
|
1545
|
+
if (client !== this.client || version !== this.roomVersion) throw this.cancelled();
|
|
1546
|
+
return track.getMediaStreamTrack();
|
|
1547
|
+
}
|
|
1548
|
+
/**
|
|
1549
|
+
* 保存音频订阅意图;音频稍后发布时自动订阅,播放前应用音量。
|
|
1550
|
+
*/
|
|
1551
|
+
async subscribeRemoteAudio(userID, subscribe) {
|
|
1552
|
+
const client = this.requireClient();
|
|
1553
|
+
const user = client.remoteUsers.find((user2) => String(user2.uid) === userID);
|
|
1554
|
+
if (!subscribe) {
|
|
1555
|
+
this.requestedAudio.delete(userID);
|
|
1556
|
+
user?.audioTrack?.stop();
|
|
1557
|
+
if (user?.audioTrack && !this.rejoinTask) await this.run(() => client.unsubscribe(user, "audio"));
|
|
1558
|
+
return;
|
|
1559
|
+
}
|
|
1560
|
+
this.requestedAudio.add(userID);
|
|
1561
|
+
if (this.rejoinOperation) {
|
|
1562
|
+
const version2 = this.roomVersion;
|
|
1563
|
+
await this.rejoinOperation;
|
|
1564
|
+
this.ensureRoomCurrent(client, version2);
|
|
1565
|
+
if (this.requestedAudio.has(userID)) await this.subscribeRemoteAudio(userID, true);
|
|
1566
|
+
return;
|
|
1567
|
+
}
|
|
1568
|
+
if (!user?.hasAudio) return;
|
|
1569
|
+
const version = this.roomVersion;
|
|
1570
|
+
const track = await this.run(() => client.subscribe(user, "audio"));
|
|
1571
|
+
if (client !== this.client || version !== this.roomVersion || !this.requestedAudio.has(userID)) {
|
|
1572
|
+
track.stop();
|
|
1573
|
+
return;
|
|
1574
|
+
}
|
|
1575
|
+
this.applyAudioVolume(user);
|
|
1576
|
+
}
|
|
1577
|
+
/**
|
|
1578
|
+
* 缓存音量,订阅前可调用;零音量不调用 play,防止初始短暂漏声。
|
|
1579
|
+
*/
|
|
1580
|
+
setRemoteAudioVolume(volume, userID) {
|
|
1581
|
+
this.audioVolumes.set(userID, Math.max(0, Math.min(100, Number.isFinite(volume) ? Math.round(volume) : 0)));
|
|
1582
|
+
const user = this.client?.remoteUsers.find((user2) => String(user2.uid) === userID);
|
|
1583
|
+
if (this.rejoinTask || this.tokenExpired) {
|
|
1584
|
+
user?.audioTrack?.stop();
|
|
1585
|
+
return;
|
|
1586
|
+
}
|
|
1587
|
+
if (user) this.applyAudioVolume(user);
|
|
1588
|
+
}
|
|
1589
|
+
/**
|
|
1590
|
+
* 应用当前用户的播放状态,保留声网对浏览器自动播放限制的处理。
|
|
1591
|
+
*/
|
|
1592
|
+
applyAudioVolume(user) {
|
|
1593
|
+
const track = user.audioTrack;
|
|
1594
|
+
if (!track) return;
|
|
1595
|
+
const volume = this.audioVolumes.get(String(user.uid)) ?? 0;
|
|
1596
|
+
track.setVolume(volume);
|
|
1597
|
+
if (volume > 0 && this.requestedAudio.has(String(user.uid))) track.play();
|
|
1598
|
+
else track.stop();
|
|
1599
|
+
}
|
|
1600
|
+
/**
|
|
1601
|
+
* 按房间顺序发送消息,限制每秒包数与总字节;失败返回给原业务调用。
|
|
1602
|
+
*/
|
|
1603
|
+
sendRoomMessage(message) {
|
|
1604
|
+
const version = this.roomVersion;
|
|
1605
|
+
const client = this.client;
|
|
1606
|
+
const bytes = new TextEncoder().encode(message).byteLength;
|
|
1607
|
+
const operation = this.sendTail.then(async () => {
|
|
1608
|
+
await this.rejoinOperation;
|
|
1609
|
+
if (!client || client !== this.client || !this.connection || version !== this.roomVersion || this.tokenExpired) throw this.cancelled();
|
|
1610
|
+
if (bytes > 1e3) throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Room message exceeds the 1000-byte limit");
|
|
1611
|
+
await this.waitForSendBudget(bytes, version);
|
|
1612
|
+
await this.rejoinOperation;
|
|
1613
|
+
if (client !== this.client || version !== this.roomVersion || !this.connection || this.tokenExpired) throw this.cancelled();
|
|
1614
|
+
this.sentPackets.push({ time: Date.now(), bytes });
|
|
1615
|
+
await this.sendWithTimeout(client, message);
|
|
1616
|
+
});
|
|
1617
|
+
this.sendTail = operation.catch(() => {
|
|
1618
|
+
});
|
|
1619
|
+
return operation;
|
|
1620
|
+
}
|
|
1621
|
+
/**
|
|
1622
|
+
* 发送超过五秒视为失败;退房立即取消等待,底层迟到结果不影响新房间。
|
|
1623
|
+
*/
|
|
1624
|
+
async sendWithTimeout(client, message) {
|
|
1625
|
+
await new Promise((resolve, reject) => {
|
|
1626
|
+
const finish = (error) => {
|
|
1627
|
+
clearTimeout(timer);
|
|
1628
|
+
if (this.cancelActiveSend === cancel) this.cancelActiveSend = void 0;
|
|
1629
|
+
if (error) reject(this.mapError(error));
|
|
1630
|
+
else resolve();
|
|
1631
|
+
};
|
|
1632
|
+
const cancel = () => finish(this.cancelled());
|
|
1633
|
+
const timer = setTimeout(() => finish(new XmaxError("TIMEOUT" /* timeout */, "Agora room message send timed out")), 5e3);
|
|
1634
|
+
this.cancelActiveSend = cancel;
|
|
1635
|
+
void this.run(() => client.sendStreamMessage(message, false)).then(() => finish(), finish);
|
|
1636
|
+
});
|
|
1637
|
+
}
|
|
1638
|
+
/**
|
|
1639
|
+
* 按滑动时间窗限速,退房立即唤醒等待并取消,不遗留计时器。
|
|
1640
|
+
*/
|
|
1641
|
+
async waitForSendBudget(bytes, version) {
|
|
1642
|
+
while (version === this.roomVersion) {
|
|
1643
|
+
const now = Date.now();
|
|
1644
|
+
this.sentPackets = this.sentPackets.filter((packet) => now - packet.time < 1e3);
|
|
1645
|
+
if (this.sentPackets.length < 30 && this.sentPackets.reduce((sum, packet) => sum + packet.bytes, bytes) <= 6e3) return;
|
|
1646
|
+
await new Promise((resolve) => {
|
|
1647
|
+
const finish = () => {
|
|
1648
|
+
clearTimeout(timer);
|
|
1649
|
+
this.cancelSendDelay = void 0;
|
|
1650
|
+
resolve();
|
|
1651
|
+
};
|
|
1652
|
+
const timer = setTimeout(finish, Math.max(1, this.sentPackets[0].time + 1e3 - now));
|
|
1653
|
+
this.cancelSendDelay = finish;
|
|
1654
|
+
});
|
|
1655
|
+
}
|
|
1656
|
+
throw this.cancelled();
|
|
1657
|
+
}
|
|
1658
|
+
/**
|
|
1659
|
+
* 设置公共 RTC 监听器。
|
|
1660
|
+
*/
|
|
1661
|
+
setEventListener(listener) {
|
|
1662
|
+
this.listener = listener;
|
|
1663
|
+
}
|
|
1664
|
+
/**
|
|
1665
|
+
* 将声网发布、DataStream、网络质量与 Token 事件桥接到公共传输层。
|
|
1666
|
+
*/
|
|
1667
|
+
registerEvents(client) {
|
|
1668
|
+
const active = () => this.client === client && this.connection !== void 0 && !this.rejoinTask;
|
|
1669
|
+
const mediaActive = () => active() && !this.tokenExpired;
|
|
1670
|
+
const handleExpired = () => {
|
|
1671
|
+
if (!active() || this.tokenExpired) return;
|
|
1672
|
+
this.tokenExpired = true;
|
|
1673
|
+
this.listener?.onTokenExpired?.();
|
|
1674
|
+
};
|
|
1675
|
+
client.on("user-published", (user, kind) => {
|
|
1676
|
+
if (!mediaActive()) return;
|
|
1677
|
+
if (kind === "video") this.listener?.onRemoteVideoPublished(String(user.uid), true);
|
|
1678
|
+
if (kind === "audio" && this.requestedAudio.has(String(user.uid))) {
|
|
1679
|
+
void this.subscribeRemoteAudio(String(user.uid), true).catch((error) => {
|
|
1680
|
+
if (active()) this.listener?.onError?.(this.mapError(error));
|
|
1681
|
+
});
|
|
1682
|
+
}
|
|
1683
|
+
});
|
|
1684
|
+
client.on("user-unpublished", (user, kind) => {
|
|
1685
|
+
if (!mediaActive()) return;
|
|
1686
|
+
if (kind === "video") this.listener?.onRemoteVideoPublished(String(user.uid), false);
|
|
1687
|
+
if (kind === "audio") user.audioTrack?.stop();
|
|
1688
|
+
});
|
|
1689
|
+
client.on("user-left", (user) => {
|
|
1690
|
+
if (mediaActive()) this.listener?.onRemoteVideoPublished(String(user.uid), false);
|
|
1691
|
+
});
|
|
1692
|
+
client.on("stream-message", (uid, data) => {
|
|
1693
|
+
if (mediaActive()) this.listener?.onCustomMessageReceived(String(uid), typeof data === "string" ? data : new TextDecoder().decode(data));
|
|
1694
|
+
});
|
|
1695
|
+
client.on("network-quality", (quality) => {
|
|
1696
|
+
if (mediaActive()) AgoraRtcStatistics.network(quality, this.listener);
|
|
1697
|
+
});
|
|
1698
|
+
client.on("token-privilege-will-expire", () => {
|
|
1699
|
+
if (active() && !this.tokenExpired) this.listener?.onTokenWillExpire?.();
|
|
1700
|
+
});
|
|
1701
|
+
client.on("token-privilege-did-expire", handleExpired);
|
|
1702
|
+
client.on("connection-state-change", (state2, previous, reason) => {
|
|
1703
|
+
if (state2 === "DISCONNECTED" && reason === "TOKEN_EXPIRE") {
|
|
1704
|
+
handleExpired();
|
|
1705
|
+
return;
|
|
1706
|
+
}
|
|
1707
|
+
if (active() && !this.tokenExpired && state2 === "DISCONNECTED" && previous !== "DISCONNECTED") {
|
|
1708
|
+
this.listener?.onError?.(new XmaxError("RTC_ERROR" /* rtcError */, `Agora connection disconnected: ${reason ?? "unknown"}`));
|
|
1709
|
+
}
|
|
1710
|
+
});
|
|
1711
|
+
}
|
|
1712
|
+
/**
|
|
1713
|
+
* 清理房间状态、播放和发送队列,不释放本地视频采集。
|
|
1714
|
+
*/
|
|
1715
|
+
clearRoom() {
|
|
1716
|
+
this.roomVersion++;
|
|
1717
|
+
this.cancelRejoin?.();
|
|
1718
|
+
this.connection = void 0;
|
|
1719
|
+
this.tokenExpired = false;
|
|
1720
|
+
this.videoPublishRequested = false;
|
|
1721
|
+
this.audioPublishRequested = false;
|
|
1722
|
+
clearInterval(this.statisticsTimer);
|
|
1723
|
+
this.statisticsTimer = void 0;
|
|
1724
|
+
this.cancelSendDelay?.();
|
|
1725
|
+
this.cancelActiveSend?.();
|
|
1726
|
+
this.sentPackets = [];
|
|
1727
|
+
this.sendTail = Promise.resolve();
|
|
1728
|
+
this.requestedAudio.clear();
|
|
1729
|
+
this.audioVolumes.clear();
|
|
1730
|
+
for (const user of this.client?.remoteUsers ?? []) user.audioTrack?.stop();
|
|
1731
|
+
}
|
|
1732
|
+
/**
|
|
1733
|
+
* 判断异步恢复工作是否仍属于当前房间生命周期。
|
|
1734
|
+
*/
|
|
1735
|
+
isRoomCurrent(client, version) {
|
|
1736
|
+
return this.client === client && this.connection !== void 0 && this.roomVersion === version;
|
|
1737
|
+
}
|
|
1738
|
+
/**
|
|
1739
|
+
* 退房、销毁或换房后停止后续恢复步骤。
|
|
1740
|
+
*/
|
|
1741
|
+
ensureRoomCurrent(client, version) {
|
|
1742
|
+
if (!this.isRoomCurrent(client, version)) throw this.cancelled();
|
|
1743
|
+
}
|
|
1744
|
+
/**
|
|
1745
|
+
* 获取已初始化的 client。
|
|
1746
|
+
*/
|
|
1747
|
+
requireClient() {
|
|
1748
|
+
if (!this.client) throw new XmaxError("RTC_ERROR" /* rtcError */, "Agora is not initialized");
|
|
1749
|
+
return this.client;
|
|
1750
|
+
}
|
|
1751
|
+
/**
|
|
1752
|
+
* 获取已启动的相机采集。
|
|
1753
|
+
*/
|
|
1754
|
+
requireCamera() {
|
|
1755
|
+
if (!this.camera) throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Camera capture is not running");
|
|
1756
|
+
return this.camera;
|
|
1757
|
+
}
|
|
1758
|
+
/**
|
|
1759
|
+
* 执行声网操作并统一错误类型。
|
|
1760
|
+
*/
|
|
1761
|
+
async run(operation) {
|
|
1762
|
+
try {
|
|
1763
|
+
return await operation();
|
|
1764
|
+
} catch (error) {
|
|
1765
|
+
throw this.mapError(error);
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
/**
|
|
1769
|
+
* 只传递错误码,不回显可能包含 Token 的厂商原始错误消息。
|
|
1770
|
+
*/
|
|
1771
|
+
mapError(error, permissionCode) {
|
|
1772
|
+
if (error instanceof XmaxError) return error;
|
|
1773
|
+
const code = typeof error === "object" && error !== null && "code" in error ? String(error.code) : "UNKNOWN";
|
|
1774
|
+
const safeCode = /^[A-Z_0-9-]+$/.test(code) ? code : "UNKNOWN";
|
|
1775
|
+
return new XmaxError(permissionCode && safeCode === "PERMISSION_DENIED" ? permissionCode : "RTC_ERROR" /* rtcError */, `Agora operation failed (${safeCode})`);
|
|
1776
|
+
}
|
|
1777
|
+
/**
|
|
1778
|
+
* 构造异步操作失效错误。
|
|
1779
|
+
*/
|
|
1780
|
+
cancelled() {
|
|
1781
|
+
return new XmaxError("CANCELLED" /* cancelled */, "Agora operation was cancelled");
|
|
1782
|
+
}
|
|
1783
|
+
};
|
|
1784
|
+
/**
|
|
1785
|
+
* 模块级区域约束:setArea 是全局设置,拒绝同时运行不同区域,避免静默覆盖。
|
|
1786
|
+
*/
|
|
1787
|
+
__publicField(_AgoraRtcManager, "regions", /* @__PURE__ */ new Map());
|
|
1788
|
+
var AgoraRtcManager = _AgoraRtcManager;
|
|
1789
|
+
|
|
1790
|
+
// src/Foundation/RTC/RtcProvider.ts
|
|
1791
|
+
var RtcProvider = /* @__PURE__ */ ((RtcProvider2) => {
|
|
1792
|
+
RtcProvider2["trtc"] = "trtc";
|
|
1793
|
+
RtcProvider2["agora"] = "agora";
|
|
1794
|
+
RtcProvider2["vertc"] = "vertc";
|
|
1795
|
+
return RtcProvider2;
|
|
1796
|
+
})(RtcProvider || {});
|
|
1797
|
+
|
|
1798
|
+
// src/Foundation/RTC/TRTC/RtcEngineManager.ts
|
|
1075
1799
|
var RtcEngineLease = class {
|
|
1076
1800
|
/**
|
|
1077
1801
|
* 为已创建的引擎生成独占租约标识,供引擎管理器核对释放请求。
|
|
@@ -1304,34 +2028,15 @@ var RtcStatsLogger = class {
|
|
|
1304
2028
|
}
|
|
1305
2029
|
};
|
|
1306
2030
|
|
|
1307
|
-
// src/Foundation/RTC/
|
|
1308
|
-
function
|
|
1309
|
-
const userAgent = environment?.userAgent ?? (typeof navigator === "undefined" ? "" : navigator.userAgent);
|
|
1310
|
-
const maxTouchPoints = environment?.maxTouchPoints ?? (typeof navigator === "undefined" ? 0 : navigator.maxTouchPoints ?? 0);
|
|
1311
|
-
const mobile = /Android|webOS|iPhone|iPad|iPod|Mobile/i.test(userAgent) || /Macintosh/i.test(userAgent) && maxTouchPoints > 1;
|
|
1312
|
-
if (!mobile) {
|
|
1313
|
-
return false;
|
|
1314
|
-
}
|
|
1315
|
-
const orientationType = environment?.orientationType ?? (typeof window === "undefined" ? void 0 : window.screen?.orientation?.type);
|
|
1316
|
-
if (orientationType !== void 0) {
|
|
1317
|
-
return orientationType.includes("portrait");
|
|
1318
|
-
}
|
|
1319
|
-
const degrees = environment?.orientationDegrees ?? (typeof window === "undefined" ? void 0 : window.orientation);
|
|
1320
|
-
return degrees === 0 || degrees === 180;
|
|
1321
|
-
}
|
|
1322
|
-
function rtcOrientedVideoSize(size, environment) {
|
|
1323
|
-
return shouldTransposeRtcVideoSize(environment) ? { width: size.height, height: size.width } : size;
|
|
1324
|
-
}
|
|
1325
|
-
|
|
1326
|
-
// src/Foundation/RTC/RemoteAudioVolumeControl.ts
|
|
1327
|
-
function canControlRemoteAudioVolume(environment) {
|
|
2031
|
+
// src/Foundation/RTC/TRTC/TrtcAudioVolumeSupport.ts
|
|
2032
|
+
function canControlTrtcRemoteAudioVolume(environment) {
|
|
1328
2033
|
const userAgent = environment?.userAgent ?? (typeof navigator === "undefined" ? "" : navigator.userAgent);
|
|
1329
2034
|
const maxTouchPoints = environment?.maxTouchPoints ?? (typeof navigator === "undefined" ? 0 : navigator.maxTouchPoints ?? 0);
|
|
1330
2035
|
const ios = /iPhone|iPad|iPod/i.test(userAgent) || /Macintosh/i.test(userAgent) && maxTouchPoints > 1;
|
|
1331
2036
|
return !ios;
|
|
1332
2037
|
}
|
|
1333
2038
|
|
|
1334
|
-
// src/Foundation/RTC/
|
|
2039
|
+
// src/Foundation/RTC/TRTC/TrtcRtcManager.ts
|
|
1335
2040
|
var RTC_EVENT = {
|
|
1336
2041
|
remoteVideoAvailable: "remote-video-available",
|
|
1337
2042
|
remoteVideoUnavailable: "remote-video-unavailable",
|
|
@@ -1347,7 +2052,7 @@ var QOS_PREFERENCE_MAP = {
|
|
|
1347
2052
|
["maintainFramerate" /* maintainFramerate */]: "smooth",
|
|
1348
2053
|
["maintainQuality" /* maintainQuality */]: "clear"
|
|
1349
2054
|
};
|
|
1350
|
-
var
|
|
2055
|
+
var TrtcRtcManager = class {
|
|
1351
2056
|
/**
|
|
1352
2057
|
* 创建 RTC 管理器。
|
|
1353
2058
|
*
|
|
@@ -1515,6 +2220,9 @@ var RtcManager = class {
|
|
|
1515
2220
|
* @throws 引擎未初始化、进房参数无效或进房失败时抛出错误。
|
|
1516
2221
|
*/
|
|
1517
2222
|
async joinRoom(configuration) {
|
|
2223
|
+
if (configuration.provider !== "trtc") {
|
|
2224
|
+
throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "TRTC credentials required");
|
|
2225
|
+
}
|
|
1518
2226
|
const engine = this.requireEngine();
|
|
1519
2227
|
const sdkAppId = Number(configuration.sdkAppID);
|
|
1520
2228
|
if (!Number.isInteger(sdkAppId) || sdkAppId <= 0) {
|
|
@@ -1711,7 +2419,7 @@ var RtcManager = class {
|
|
|
1711
2419
|
* 当前平台是否支持远端音量控制(iOS Safari/WKWebView 不支持)。
|
|
1712
2420
|
*/
|
|
1713
2421
|
get supportsRemoteAudioVolumeControl() {
|
|
1714
|
-
return
|
|
2422
|
+
return canControlTrtcRemoteAudioVolume();
|
|
1715
2423
|
}
|
|
1716
2424
|
/**
|
|
1717
2425
|
* 设置指定远端用户的音频播放音量。
|
|
@@ -1726,7 +2434,7 @@ var RtcManager = class {
|
|
|
1726
2434
|
setRemoteAudioVolume(volume, userID) {
|
|
1727
2435
|
const engine = this.requireEngine();
|
|
1728
2436
|
const normalized = Math.min(Math.max(Math.round(volume), 0), 100);
|
|
1729
|
-
if (!
|
|
2437
|
+
if (!canControlTrtcRemoteAudioVolume()) {
|
|
1730
2438
|
void engine.muteRemoteAudio(userID, normalized === 0).catch((error) => {
|
|
1731
2439
|
XmaxLogger.rtc.warning(
|
|
1732
2440
|
() => `\u8FDC\u7AEF\u97F3\u9891\u64AD\u653E\u72B6\u6001\u5207\u6362\u5931\u8D25 (Failed to Toggle Remote Audio Playback)
|
|
@@ -1742,7 +2450,7 @@ var RtcManager = class {
|
|
|
1742
2450
|
*
|
|
1743
2451
|
* @throws 未在房间中或消息编码后超过 1000 字节时抛出错误。
|
|
1744
2452
|
*/
|
|
1745
|
-
sendRoomMessage(message) {
|
|
2453
|
+
async sendRoomMessage(message) {
|
|
1746
2454
|
const engine = this.requireEngine();
|
|
1747
2455
|
if (!this.isInRoom) {
|
|
1748
2456
|
throw new XmaxError(
|
|
@@ -1762,6 +2470,11 @@ var RtcManager = class {
|
|
|
1762
2470
|
data: encoded.buffer
|
|
1763
2471
|
});
|
|
1764
2472
|
}
|
|
2473
|
+
/**
|
|
2474
|
+
* TRTC 凭据由会话层缓存,保持现有进房鉴权行为。
|
|
2475
|
+
*/
|
|
2476
|
+
async updateCredentials(_configuration) {
|
|
2477
|
+
}
|
|
1765
2478
|
/**
|
|
1766
2479
|
* 设置 RTC 事件监听器,传入空值时清除监听器。
|
|
1767
2480
|
*/
|
|
@@ -1838,53 +2551,679 @@ var RtcManager = class {
|
|
|
1838
2551
|
}
|
|
1839
2552
|
this.eventListener?.onRemoteVideoPublished(event.userId, false);
|
|
1840
2553
|
});
|
|
1841
|
-
source.on(RTC_EVENT.customMessage, (event) => {
|
|
1842
|
-
if (event.cmdId !== CUSTOM_MESSAGE_CMD_ID) {
|
|
1843
|
-
return;
|
|
1844
|
-
}
|
|
1845
|
-
let message;
|
|
1846
|
-
try {
|
|
1847
|
-
message = new TextDecoder().decode(event.data);
|
|
1848
|
-
} catch {
|
|
1849
|
-
XmaxLogger.rtc.warning(() => "\u81EA\u5B9A\u4E49\u6D88\u606F\u89E3\u7801\u5931\u8D25 (Failed to Decode Custom Message)");
|
|
1850
|
-
return;
|
|
2554
|
+
source.on(RTC_EVENT.customMessage, (event) => {
|
|
2555
|
+
if (event.cmdId !== CUSTOM_MESSAGE_CMD_ID) {
|
|
2556
|
+
return;
|
|
2557
|
+
}
|
|
2558
|
+
let message;
|
|
2559
|
+
try {
|
|
2560
|
+
message = new TextDecoder().decode(event.data);
|
|
2561
|
+
} catch {
|
|
2562
|
+
XmaxLogger.rtc.warning(() => "\u81EA\u5B9A\u4E49\u6D88\u606F\u89E3\u7801\u5931\u8D25 (Failed to Decode Custom Message)");
|
|
2563
|
+
return;
|
|
2564
|
+
}
|
|
2565
|
+
this.eventListener?.onCustomMessageReceived(event.userId, message);
|
|
2566
|
+
});
|
|
2567
|
+
}
|
|
2568
|
+
/**
|
|
2569
|
+
* 返回当前引擎实例;未初始化时抛出错误。
|
|
2570
|
+
*/
|
|
2571
|
+
requireEngine() {
|
|
2572
|
+
const engine = this.lease?.engine;
|
|
2573
|
+
if (!engine) {
|
|
2574
|
+
throw new XmaxError(
|
|
2575
|
+
"RTC_ERROR" /* rtcError */,
|
|
2576
|
+
"RTC engine is not initialized"
|
|
2577
|
+
);
|
|
2578
|
+
}
|
|
2579
|
+
return engine;
|
|
2580
|
+
}
|
|
2581
|
+
/**
|
|
2582
|
+
* 将 TRTC 错误映射为 SDK 统一错误。
|
|
2583
|
+
*/
|
|
2584
|
+
mapError(error) {
|
|
2585
|
+
if (error instanceof XmaxError) {
|
|
2586
|
+
return error;
|
|
2587
|
+
}
|
|
2588
|
+
const rtcError = error;
|
|
2589
|
+
const originName = rtcError?.originError?.name ?? "";
|
|
2590
|
+
const message = typeof rtcError?.message === "string" && rtcError.message.trim().length > 0 ? rtcError.message.trim() : String(error);
|
|
2591
|
+
if (originName === "NotAllowedError" || originName === "SecurityError" || /NotAllowed/i.test(message)) {
|
|
2592
|
+
return new XmaxError("CAMERA_PERMISSION_DENIED" /* cameraPermissionDenied */, message);
|
|
2593
|
+
}
|
|
2594
|
+
if (originName) {
|
|
2595
|
+
return new XmaxError("MEDIA_ERROR" /* mediaError */, message);
|
|
2596
|
+
}
|
|
2597
|
+
return new XmaxError("RTC_ERROR" /* rtcError */, message);
|
|
2598
|
+
}
|
|
2599
|
+
};
|
|
2600
|
+
|
|
2601
|
+
// src/Foundation/RTC/VeRTC/VeRtcStatistics.ts
|
|
2602
|
+
var VeRtcStatistics = class {
|
|
2603
|
+
static local(stats) {
|
|
2604
|
+
return Object.freeze({
|
|
2605
|
+
width: this.valid(stats.encodedFrameWidth, 1),
|
|
2606
|
+
height: this.valid(stats.encodedFrameHeight, 1),
|
|
2607
|
+
frameRate: this.valid(stats.sentFrameRate),
|
|
2608
|
+
bitrateKbps: this.valid(stats.sentKBitrate),
|
|
2609
|
+
uplinkLossPercent: this.loss(stats.videoLossRate)
|
|
2610
|
+
});
|
|
2611
|
+
}
|
|
2612
|
+
static remote(userID, stats, uplinkLossPercent) {
|
|
2613
|
+
return Object.freeze({
|
|
2614
|
+
userID,
|
|
2615
|
+
width: this.valid(stats.width, 1),
|
|
2616
|
+
height: this.valid(stats.height, 1),
|
|
2617
|
+
frameRate: this.valid(stats.decoderOutputFrameRate),
|
|
2618
|
+
bitrateKbps: this.valid(stats.receivedKBitrate),
|
|
2619
|
+
rttMs: this.valid(stats.rtt),
|
|
2620
|
+
downlinkLossPercent: this.loss(stats.videoLossRate),
|
|
2621
|
+
endToEndDelayMs: this.valid(stats.e2eDelay),
|
|
2622
|
+
uplinkLossPercent
|
|
2623
|
+
});
|
|
2624
|
+
}
|
|
2625
|
+
static quality(value) {
|
|
2626
|
+
return Number.isInteger(value) && value >= 0 && value <= 6 ? value : void 0;
|
|
2627
|
+
}
|
|
2628
|
+
static valid(value, minimum = 0) {
|
|
2629
|
+
return typeof value === "number" && Number.isFinite(value) && value >= minimum ? value : void 0;
|
|
2630
|
+
}
|
|
2631
|
+
static loss(value) {
|
|
2632
|
+
return Number.isFinite(value) && value >= 0 && value <= 1 ? value * 100 : void 0;
|
|
2633
|
+
}
|
|
2634
|
+
};
|
|
2635
|
+
|
|
2636
|
+
// src/Foundation/RTC/VeRTC/VeRtcManager.ts
|
|
2637
|
+
var VERTC_APP_ID = "69a177e226e9b90176a86b96";
|
|
2638
|
+
var VeRtcManager = class {
|
|
2639
|
+
constructor(options = {}) {
|
|
2640
|
+
__publicField(this, "loadSDK");
|
|
2641
|
+
__publicField(this, "sdk");
|
|
2642
|
+
__publicField(this, "engine");
|
|
2643
|
+
__publicField(this, "initialization");
|
|
2644
|
+
__publicField(this, "lifecycle", 0);
|
|
2645
|
+
__publicField(this, "captureVersion", 0);
|
|
2646
|
+
__publicField(this, "camera");
|
|
2647
|
+
__publicField(this, "microphone", false);
|
|
2648
|
+
__publicField(this, "connection");
|
|
2649
|
+
__publicField(this, "joined", false);
|
|
2650
|
+
__publicField(this, "roomVersion", 0);
|
|
2651
|
+
__publicField(this, "roomAbort", new AbortController());
|
|
2652
|
+
__publicField(this, "recovery");
|
|
2653
|
+
__publicField(this, "joining");
|
|
2654
|
+
__publicField(this, "tokenExpired", false);
|
|
2655
|
+
__publicField(this, "publishPrivilegeExpired", false);
|
|
2656
|
+
__publicField(this, "subscribePrivilegeExpired", false);
|
|
2657
|
+
__publicField(this, "videoRequested", false);
|
|
2658
|
+
__publicField(this, "audioRequested", false);
|
|
2659
|
+
__publicField(this, "listener");
|
|
2660
|
+
__publicField(this, "publishedVideo", /* @__PURE__ */ new Set());
|
|
2661
|
+
__publicField(this, "publishedAudio", /* @__PURE__ */ new Set());
|
|
2662
|
+
__publicField(this, "requestedVideo", /* @__PURE__ */ new Set());
|
|
2663
|
+
__publicField(this, "requestedAudio", /* @__PURE__ */ new Set());
|
|
2664
|
+
__publicField(this, "audioVolumes", /* @__PURE__ */ new Map());
|
|
2665
|
+
__publicField(this, "audioPlayers", /* @__PURE__ */ new Map());
|
|
2666
|
+
__publicField(this, "audioSubscriptions", /* @__PURE__ */ new Map());
|
|
2667
|
+
__publicField(this, "remoteStatistics", /* @__PURE__ */ new Map());
|
|
2668
|
+
__publicField(this, "uplinkLossPercent");
|
|
2669
|
+
this.loadSDK = options.loadSDK ?? (() => import("@volcengine/rtc"));
|
|
2670
|
+
}
|
|
2671
|
+
get isInitialized() {
|
|
2672
|
+
return this.engine !== void 0;
|
|
2673
|
+
}
|
|
2674
|
+
get supportsRemoteAudioVolumeControl() {
|
|
2675
|
+
if (typeof navigator === "undefined") return true;
|
|
2676
|
+
return !(/iPad|iPhone|iPod/.test(navigator.userAgent) || /Macintosh/.test(navigator.userAgent) && navigator.maxTouchPoints > 1);
|
|
2677
|
+
}
|
|
2678
|
+
async initialize() {
|
|
2679
|
+
if (this.engine) return;
|
|
2680
|
+
if (this.initialization) return this.initialization;
|
|
2681
|
+
const version = this.lifecycle;
|
|
2682
|
+
const operation = (async () => {
|
|
2683
|
+
const sdk = await this.loadSDK();
|
|
2684
|
+
if (version !== this.lifecycle) throw this.cancelled();
|
|
2685
|
+
sdk.default.setLogConfig({ logLevel: "none" });
|
|
2686
|
+
const engine = sdk.default.createEngine(VERTC_APP_ID, { autoPlayPolicy: sdk.RTCAutoPlayPolicy.PLAY_MANUALLY });
|
|
2687
|
+
this.sdk = sdk;
|
|
2688
|
+
this.engine = engine;
|
|
2689
|
+
this.registerEvents(engine);
|
|
2690
|
+
})();
|
|
2691
|
+
this.initialization = operation;
|
|
2692
|
+
try {
|
|
2693
|
+
await operation;
|
|
2694
|
+
} finally {
|
|
2695
|
+
if (this.initialization === operation) this.initialization = void 0;
|
|
2696
|
+
}
|
|
2697
|
+
}
|
|
2698
|
+
async destroy() {
|
|
2699
|
+
this.lifecycle++;
|
|
2700
|
+
this.captureVersion++;
|
|
2701
|
+
const engine = this.engine;
|
|
2702
|
+
const sdk = this.sdk;
|
|
2703
|
+
this.listener = void 0;
|
|
2704
|
+
this.clearRoom();
|
|
2705
|
+
this.engine = void 0;
|
|
2706
|
+
this.sdk = void 0;
|
|
2707
|
+
this.camera = void 0;
|
|
2708
|
+
this.microphone = false;
|
|
2709
|
+
engine?.removeAllListeners();
|
|
2710
|
+
if (engine && sdk) sdk.default.destroyEngine(engine);
|
|
2711
|
+
}
|
|
2712
|
+
async startCameraCapture(options) {
|
|
2713
|
+
const engine = this.requireEngine();
|
|
2714
|
+
if (this.camera) throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Camera capture is already running");
|
|
2715
|
+
const version = ++this.captureVersion;
|
|
2716
|
+
await this.run(() => engine.setVideoEncoderConfig({
|
|
2717
|
+
...rtcOrientedVideoSize({ width: options.width, height: options.height }),
|
|
2718
|
+
frameRate: options.frameRate,
|
|
2719
|
+
maxKbps: 1e3,
|
|
2720
|
+
preferCodecName: this.sdk.VideoCodecType.H264,
|
|
2721
|
+
contentHint: "motion"
|
|
2722
|
+
}));
|
|
2723
|
+
if (engine !== this.engine || version !== this.captureVersion) throw this.cancelled();
|
|
2724
|
+
try {
|
|
2725
|
+
await engine.startVideoCapture(options.position === "front" /* front */ ? "user" : "environment");
|
|
2726
|
+
if (engine !== this.engine || version !== this.captureVersion) {
|
|
2727
|
+
await engine.stopVideoCapture().catch(() => {
|
|
2728
|
+
});
|
|
2729
|
+
throw this.cancelled();
|
|
2730
|
+
}
|
|
2731
|
+
const track = engine.getLocalStreamTrack(this.sdk.StreamIndex.STREAM_INDEX_MAIN, "video");
|
|
2732
|
+
if (!track) {
|
|
2733
|
+
await engine.stopVideoCapture();
|
|
2734
|
+
throw new XmaxError("RTC_ERROR" /* rtcError */, "VeRTC camera track is unavailable");
|
|
2735
|
+
}
|
|
2736
|
+
this.camera = track;
|
|
2737
|
+
return track;
|
|
2738
|
+
} catch (error) {
|
|
2739
|
+
throw this.mapError(error, "CAMERA_PERMISSION_DENIED" /* cameraPermissionDenied */);
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2742
|
+
async switchCameraCapture(to) {
|
|
2743
|
+
const engine = this.requireEngine();
|
|
2744
|
+
this.requireCamera();
|
|
2745
|
+
const version = this.captureVersion;
|
|
2746
|
+
await this.run(() => engine.setVideoCaptureDevice(to === "front" /* front */ ? "user" : "environment"));
|
|
2747
|
+
if (engine !== this.engine || version !== this.captureVersion) throw this.cancelled();
|
|
2748
|
+
const track = engine.getLocalStreamTrack(this.sdk.StreamIndex.STREAM_INDEX_MAIN, "video");
|
|
2749
|
+
if (!track) throw new XmaxError("RTC_ERROR" /* rtcError */, "VeRTC camera track is unavailable");
|
|
2750
|
+
this.camera = track;
|
|
2751
|
+
return track;
|
|
2752
|
+
}
|
|
2753
|
+
async stopCameraCapture() {
|
|
2754
|
+
this.captureVersion++;
|
|
2755
|
+
this.camera = void 0;
|
|
2756
|
+
if (this.engine) await this.run(() => this.engine.stopVideoCapture());
|
|
2757
|
+
}
|
|
2758
|
+
async configureVideoEncoding(config) {
|
|
2759
|
+
const engine = this.requireEngine();
|
|
2760
|
+
this.requireCamera();
|
|
2761
|
+
await this.run(() => engine.setVideoEncoderConfig({
|
|
2762
|
+
...rtcOrientedVideoSize({ width: config.width, height: config.height }),
|
|
2763
|
+
frameRate: config.frameRate,
|
|
2764
|
+
maxKbps: config.maximumBitrate,
|
|
2765
|
+
contentHint: config.encoderPreference === "maintainFramerate" /* maintainFramerate */ ? "motion" : "detail",
|
|
2766
|
+
preferCodecName: this.sdk.VideoCodecType.H264
|
|
2767
|
+
}));
|
|
2768
|
+
}
|
|
2769
|
+
async joinRoom(config) {
|
|
2770
|
+
await this.joining?.catch(() => {
|
|
2771
|
+
});
|
|
2772
|
+
await this.recovery?.catch(() => {
|
|
2773
|
+
});
|
|
2774
|
+
const engine = this.requireEngine();
|
|
2775
|
+
if (config.provider !== "vertc" || config.appID !== VERTC_APP_ID || this.connection) {
|
|
2776
|
+
throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "VeRTC credentials for the configured AppID and an idle room are required");
|
|
2777
|
+
}
|
|
2778
|
+
this.clearRoom();
|
|
2779
|
+
this.connection = config;
|
|
2780
|
+
const version = this.roomVersion;
|
|
2781
|
+
const task = this.performJoin(engine, config, version);
|
|
2782
|
+
this.joining = task;
|
|
2783
|
+
try {
|
|
2784
|
+
await task;
|
|
2785
|
+
} finally {
|
|
2786
|
+
if (this.joining === task) this.joining = void 0;
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
async performJoin(engine, config, version) {
|
|
2790
|
+
try {
|
|
2791
|
+
await this.enter(engine, config);
|
|
2792
|
+
if (!this.isCurrent(engine, version)) {
|
|
2793
|
+
await engine.leaveRoom().catch(() => {
|
|
2794
|
+
});
|
|
2795
|
+
throw this.cancelled();
|
|
2796
|
+
}
|
|
2797
|
+
this.joined = true;
|
|
2798
|
+
this.replayVideo();
|
|
2799
|
+
} catch (error) {
|
|
2800
|
+
if (this.isCurrent(engine, version)) this.clearRoom();
|
|
2801
|
+
throw this.mapError(error);
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
enter(engine, config) {
|
|
2805
|
+
return this.run(() => engine.joinRoom(config.roomToken, config.roomID, { userId: config.userID }, {
|
|
2806
|
+
isAutoPublish: false,
|
|
2807
|
+
isAutoSubscribeAudio: false,
|
|
2808
|
+
isAutoSubscribeVideo: false,
|
|
2809
|
+
roomProfileType: this.sdk.RoomProfileType.communication
|
|
2810
|
+
}));
|
|
2811
|
+
}
|
|
2812
|
+
async leaveRoom() {
|
|
2813
|
+
const engine = this.engine;
|
|
2814
|
+
this.clearRoom();
|
|
2815
|
+
if (engine) await this.run(() => engine.leaveRoom());
|
|
2816
|
+
}
|
|
2817
|
+
/** 正常续签仅 updateToken;被移出房间后重进,恢复发布与订阅意图。 */
|
|
2818
|
+
async updateCredentials(config, signal) {
|
|
2819
|
+
const engine = this.requireEngine();
|
|
2820
|
+
const current = this.connection;
|
|
2821
|
+
if (signal?.aborted) throw this.cancelled();
|
|
2822
|
+
if (!current || config.provider !== "vertc" || config.appID !== current.appID || config.roomID !== current.roomID || config.userID !== current.userID) {
|
|
2823
|
+
throw new XmaxError("SESSION_ERROR" /* sessionError */, "VeRTC session binding changed");
|
|
2824
|
+
}
|
|
2825
|
+
const version = this.roomVersion;
|
|
2826
|
+
if (this.recovery) await this.cancellable(this.recovery, signal);
|
|
2827
|
+
if (!this.isCurrent(engine, version)) throw this.cancelled();
|
|
2828
|
+
const task = this.renew(engine, config, version);
|
|
2829
|
+
this.recovery = task;
|
|
2830
|
+
void task.finally(() => {
|
|
2831
|
+
if (this.recovery === task) this.recovery = void 0;
|
|
2832
|
+
}).catch(() => {
|
|
2833
|
+
});
|
|
2834
|
+
await this.cancellable(task, signal);
|
|
2835
|
+
}
|
|
2836
|
+
async renew(engine, config, version) {
|
|
2837
|
+
if (!this.tokenExpired) {
|
|
2838
|
+
try {
|
|
2839
|
+
await this.run(() => engine.updateToken(config.roomToken));
|
|
2840
|
+
} catch (error) {
|
|
2841
|
+
this.ensureCurrent(engine, version);
|
|
2842
|
+
if (!this.tokenExpired) throw error;
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
this.ensureCurrent(engine, version);
|
|
2846
|
+
const restore = this.tokenExpired || this.publishPrivilegeExpired || this.subscribePrivilegeExpired;
|
|
2847
|
+
if (restore) {
|
|
2848
|
+
this.joined = false;
|
|
2849
|
+
this.listener?.onRoomRejoining?.();
|
|
2850
|
+
this.clearPlayers();
|
|
2851
|
+
this.clearStatistics();
|
|
2852
|
+
if (this.tokenExpired) {
|
|
2853
|
+
this.publishedVideo.clear();
|
|
2854
|
+
this.publishedAudio.clear();
|
|
2855
|
+
await this.run(() => engine.leaveRoom());
|
|
2856
|
+
this.ensureCurrent(engine, version);
|
|
2857
|
+
await this.enter(engine, config);
|
|
2858
|
+
if (!this.isCurrent(engine, version)) {
|
|
2859
|
+
await engine.leaveRoom().catch(() => {
|
|
2860
|
+
});
|
|
2861
|
+
throw this.cancelled();
|
|
2862
|
+
}
|
|
2863
|
+
}
|
|
2864
|
+
if ((this.tokenExpired || this.publishPrivilegeExpired) && this.videoRequested && this.camera) {
|
|
2865
|
+
await this.run(() => engine.publishStream(this.sdk.MediaType.VIDEO));
|
|
2866
|
+
this.ensureCurrent(engine, version);
|
|
2867
|
+
if (!this.videoRequested) {
|
|
2868
|
+
await this.run(() => engine.unpublishStream(this.sdk.MediaType.VIDEO));
|
|
2869
|
+
this.ensureCurrent(engine, version);
|
|
2870
|
+
}
|
|
2871
|
+
}
|
|
2872
|
+
if ((this.tokenExpired || this.publishPrivilegeExpired) && this.audioRequested && this.microphone) {
|
|
2873
|
+
await this.run(() => engine.publishStream(this.sdk.MediaType.AUDIO));
|
|
2874
|
+
this.ensureCurrent(engine, version);
|
|
2875
|
+
if (!this.audioRequested) {
|
|
2876
|
+
await this.run(() => engine.unpublishStream(this.sdk.MediaType.AUDIO));
|
|
2877
|
+
this.ensureCurrent(engine, version);
|
|
2878
|
+
}
|
|
2879
|
+
}
|
|
2880
|
+
}
|
|
2881
|
+
this.connection = config;
|
|
2882
|
+
this.tokenExpired = false;
|
|
2883
|
+
this.publishPrivilegeExpired = false;
|
|
2884
|
+
this.subscribePrivilegeExpired = false;
|
|
2885
|
+
this.joined = true;
|
|
2886
|
+
if (restore) {
|
|
2887
|
+
this.replayVideo();
|
|
2888
|
+
for (const userID of this.requestedAudio) {
|
|
2889
|
+
if (this.publishedAudio.has(userID)) await this.subscribeRemoteAudio(userID, true);
|
|
2890
|
+
this.ensureCurrent(engine, version);
|
|
2891
|
+
}
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2894
|
+
async publishLocalVideo() {
|
|
2895
|
+
const engine = this.requireEngine();
|
|
2896
|
+
this.requireCamera();
|
|
2897
|
+
this.videoRequested = true;
|
|
2898
|
+
const version = this.roomVersion;
|
|
2899
|
+
await this.recovery;
|
|
2900
|
+
this.ensureCurrent(engine, version);
|
|
2901
|
+
if (!this.videoRequested) throw this.cancelled();
|
|
2902
|
+
await this.run(() => engine.publishStream(this.sdk.MediaType.VIDEO));
|
|
2903
|
+
}
|
|
2904
|
+
async unpublishLocalVideo() {
|
|
2905
|
+
this.videoRequested = false;
|
|
2906
|
+
if (this.joined && !this.tokenExpired) await this.run(() => this.requireEngine().unpublishStream(this.sdk.MediaType.VIDEO));
|
|
2907
|
+
}
|
|
2908
|
+
async publishLocalAudio() {
|
|
2909
|
+
const engine = this.requireEngine();
|
|
2910
|
+
this.audioRequested = true;
|
|
2911
|
+
const version = this.roomVersion;
|
|
2912
|
+
await this.recovery;
|
|
2913
|
+
this.ensureCurrent(engine, version);
|
|
2914
|
+
try {
|
|
2915
|
+
if (!this.microphone) {
|
|
2916
|
+
await engine.startAudioCapture();
|
|
2917
|
+
if (!this.isCurrent(engine, version)) {
|
|
2918
|
+
await engine.stopAudioCapture().catch(() => {
|
|
2919
|
+
});
|
|
2920
|
+
throw this.cancelled();
|
|
2921
|
+
}
|
|
2922
|
+
this.microphone = true;
|
|
2923
|
+
}
|
|
2924
|
+
if (!this.audioRequested) throw this.cancelled();
|
|
2925
|
+
await engine.publishStream(this.sdk.MediaType.AUDIO);
|
|
2926
|
+
} catch (error) {
|
|
2927
|
+
throw this.mapError(error, "MICROPHONE_PERMISSION_DENIED" /* microphonePermissionDenied */);
|
|
2928
|
+
}
|
|
2929
|
+
}
|
|
2930
|
+
async unpublishLocalAudio() {
|
|
2931
|
+
this.audioRequested = false;
|
|
2932
|
+
if (this.joined && !this.tokenExpired) await this.run(() => this.requireEngine().unpublishStream(this.sdk.MediaType.AUDIO));
|
|
2933
|
+
}
|
|
2934
|
+
async subscribeRemoteVideo(userID, subscribe) {
|
|
2935
|
+
const engine = this.requireEngine();
|
|
2936
|
+
const version = this.roomVersion;
|
|
2937
|
+
if (!subscribe) {
|
|
2938
|
+
this.requestedVideo.delete(userID);
|
|
2939
|
+
this.remoteStatistics.delete(userID);
|
|
2940
|
+
this.emitRemoteStatistics();
|
|
2941
|
+
if (this.joined && !this.tokenExpired) await this.run(() => engine.unsubscribeStream(userID, this.sdk.MediaType.VIDEO));
|
|
2942
|
+
return;
|
|
2943
|
+
}
|
|
2944
|
+
this.requestedVideo.add(userID);
|
|
2945
|
+
await this.run(() => engine.subscribeStream(userID, this.sdk.MediaType.VIDEO));
|
|
2946
|
+
this.ensureCurrent(engine, version);
|
|
2947
|
+
if (!this.requestedVideo.has(userID)) return;
|
|
2948
|
+
const track = engine.getRemoteStreamTrack(userID, this.sdk.StreamIndex.STREAM_INDEX_MAIN, "video");
|
|
2949
|
+
if (!track) throw new XmaxError("RTC_ERROR" /* rtcError */, "VeRTC remote video track is unavailable");
|
|
2950
|
+
return track;
|
|
2951
|
+
}
|
|
2952
|
+
async subscribeRemoteAudio(userID, subscribe) {
|
|
2953
|
+
const engine = this.requireEngine();
|
|
2954
|
+
const version = this.roomVersion;
|
|
2955
|
+
if (!subscribe) {
|
|
2956
|
+
this.requestedAudio.delete(userID);
|
|
2957
|
+
this.clearPlayer(userID);
|
|
2958
|
+
if (this.joined && !this.tokenExpired && this.publishedAudio.has(userID)) await this.run(() => engine.unsubscribeStream(userID, this.sdk.MediaType.AUDIO));
|
|
2959
|
+
return;
|
|
2960
|
+
}
|
|
2961
|
+
this.requestedAudio.add(userID);
|
|
2962
|
+
if (!this.joined || this.tokenExpired || !this.publishedAudio.has(userID)) return;
|
|
2963
|
+
const subscription = {};
|
|
2964
|
+
this.audioSubscriptions.set(userID, subscription);
|
|
2965
|
+
await this.run(() => engine.subscribeStream(userID, this.sdk.MediaType.AUDIO));
|
|
2966
|
+
if (!this.isCurrent(engine, version) || !this.joined || this.tokenExpired || this.audioSubscriptions.get(userID) !== subscription || !this.requestedAudio.has(userID) || !this.publishedAudio.has(userID)) return;
|
|
2967
|
+
const track = engine.getRemoteStreamTrack(userID, this.sdk.StreamIndex.STREAM_INDEX_MAIN, "audio");
|
|
2968
|
+
if (!track) return;
|
|
2969
|
+
this.clearPlayer(userID);
|
|
2970
|
+
const player = document.createElement("audio");
|
|
2971
|
+
player.srcObject = new MediaStream([track]);
|
|
2972
|
+
this.audioPlayers.set(userID, player);
|
|
2973
|
+
this.applyAudioVolume(userID);
|
|
2974
|
+
}
|
|
2975
|
+
setRemoteAudioVolume(volume, userID) {
|
|
2976
|
+
this.audioVolumes.set(userID, Number.isFinite(volume) ? Math.max(0, Math.min(100, Math.round(volume))) : 0);
|
|
2977
|
+
this.applyAudioVolume(userID);
|
|
2978
|
+
}
|
|
2979
|
+
applyAudioVolume(userID) {
|
|
2980
|
+
const player = this.audioPlayers.get(userID);
|
|
2981
|
+
if (!player) return;
|
|
2982
|
+
const volume = this.audioVolumes.get(userID) ?? 0;
|
|
2983
|
+
player.muted = volume === 0 || !this.requestedAudio.has(userID);
|
|
2984
|
+
if (this.supportsRemoteAudioVolumeControl) player.volume = volume / 100;
|
|
2985
|
+
if (player.muted) player.pause();
|
|
2986
|
+
else void player.play().catch(() => {
|
|
2987
|
+
XmaxLogger.rtc.warning(() => "VeRTC audio playback requires a user interaction");
|
|
2988
|
+
});
|
|
2989
|
+
}
|
|
2990
|
+
async sendRoomMessage(message) {
|
|
2991
|
+
const engine = this.requireEngine();
|
|
2992
|
+
const version = this.roomVersion;
|
|
2993
|
+
if (!this.joined || this.tokenExpired) throw this.cancelled();
|
|
2994
|
+
if (new TextEncoder().encode(message).byteLength > 64 * 1024) {
|
|
2995
|
+
throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "VeRTC room message exceeds the 64-KiB limit");
|
|
2996
|
+
}
|
|
2997
|
+
await this.cancellable(this.run(async () => {
|
|
2998
|
+
const sent = engine.sendRoomMessage(message);
|
|
2999
|
+
if (!sent) throw new XmaxError("RTC_ERROR" /* rtcError */, "VeRTC room message was not sent");
|
|
3000
|
+
await sent;
|
|
3001
|
+
}), void 0, 5e3);
|
|
3002
|
+
this.ensureCurrent(engine, version);
|
|
3003
|
+
}
|
|
3004
|
+
setEventListener(listener) {
|
|
3005
|
+
this.listener = listener;
|
|
3006
|
+
}
|
|
3007
|
+
registerEvents(engine) {
|
|
3008
|
+
const events = this.sdk.default.events;
|
|
3009
|
+
const active = () => engine === this.engine && this.connection !== void 0;
|
|
3010
|
+
const mediaActive = () => active() && this.joined && !this.tokenExpired;
|
|
3011
|
+
const video = (mediaType) => (mediaType & this.sdk.MediaType.VIDEO) !== 0;
|
|
3012
|
+
const audio = (mediaType) => (mediaType & this.sdk.MediaType.AUDIO) !== 0;
|
|
3013
|
+
engine.on(events.onUserPublishStream, ({ userId, mediaType }) => {
|
|
3014
|
+
if (!active()) return;
|
|
3015
|
+
if (video(mediaType)) {
|
|
3016
|
+
this.publishedVideo.add(userId);
|
|
3017
|
+
if (mediaActive()) this.listener?.onRemoteVideoPublished(userId, true);
|
|
3018
|
+
}
|
|
3019
|
+
if (audio(mediaType)) {
|
|
3020
|
+
this.publishedAudio.add(userId);
|
|
3021
|
+
if (mediaActive() && this.requestedAudio.has(userId)) {
|
|
3022
|
+
void this.subscribeRemoteAudio(userId, true).catch((error) => {
|
|
3023
|
+
if (mediaActive()) this.listener?.onError?.(this.mapError(error));
|
|
3024
|
+
});
|
|
3025
|
+
}
|
|
3026
|
+
}
|
|
3027
|
+
});
|
|
3028
|
+
engine.on(events.onUserUnpublishStream, ({ userId, mediaType }) => {
|
|
3029
|
+
if (!active()) return;
|
|
3030
|
+
if (video(mediaType)) this.removeVideo(userId);
|
|
3031
|
+
if (audio(mediaType)) {
|
|
3032
|
+
this.publishedAudio.delete(userId);
|
|
3033
|
+
this.clearPlayer(userId);
|
|
3034
|
+
}
|
|
3035
|
+
});
|
|
3036
|
+
engine.on(events.onUserLeave, ({ userInfo }) => {
|
|
3037
|
+
if (!active()) return;
|
|
3038
|
+
this.removeVideo(userInfo.userId);
|
|
3039
|
+
this.publishedAudio.delete(userInfo.userId);
|
|
3040
|
+
this.clearPlayer(userInfo.userId);
|
|
3041
|
+
});
|
|
3042
|
+
engine.on(events.onRoomMessageReceived, ({ userId, message }) => {
|
|
3043
|
+
if (mediaActive() && typeof message === "string") this.listener?.onCustomMessageReceived(userId, message);
|
|
3044
|
+
});
|
|
3045
|
+
const willExpire = () => {
|
|
3046
|
+
if (active()) this.listener?.onTokenWillExpire?.();
|
|
3047
|
+
};
|
|
3048
|
+
engine.on(events.onTokenWillExpire, willExpire);
|
|
3049
|
+
engine.on(events.onTokenPublishPrivilegeWillExpire, willExpire);
|
|
3050
|
+
engine.on(events.onTokenSubscribePrivilegeWillExpire, willExpire);
|
|
3051
|
+
engine.on(events.onTokenPublishPrivilegeDidExpired, () => {
|
|
3052
|
+
if (!active() || this.publishPrivilegeExpired) return;
|
|
3053
|
+
this.publishPrivilegeExpired = true;
|
|
3054
|
+
this.listener?.onTokenExpired?.();
|
|
3055
|
+
});
|
|
3056
|
+
engine.on(events.onTokenSubscribePrivilegeDidExpired, () => {
|
|
3057
|
+
if (!active() || this.subscribePrivilegeExpired) return;
|
|
3058
|
+
this.subscribePrivilegeExpired = true;
|
|
3059
|
+
this.clearPlayers();
|
|
3060
|
+
this.listener?.onTokenExpired?.();
|
|
3061
|
+
});
|
|
3062
|
+
engine.on(events.onError, ({ errorCode }) => {
|
|
3063
|
+
if (!active()) return;
|
|
3064
|
+
if (errorCode === "TOKEN_EXPIRED") {
|
|
3065
|
+
if (this.tokenExpired) return;
|
|
3066
|
+
this.tokenExpired = true;
|
|
3067
|
+
this.clearPlayers();
|
|
3068
|
+
this.listener?.onTokenExpired?.();
|
|
3069
|
+
} else this.listener?.onError?.(this.mapError({ code: errorCode }));
|
|
3070
|
+
});
|
|
3071
|
+
engine.on(events.onConnectionStateChanged, ({ state: state2 }) => {
|
|
3072
|
+
if (!mediaActive() || state2 !== this.sdk.ConnectionState.CONNECTION_STATE_RECONNECTED) return;
|
|
3073
|
+
this.listener?.onRoomRejoining?.();
|
|
3074
|
+
this.clearPlayers();
|
|
3075
|
+
this.clearStatistics();
|
|
3076
|
+
this.replayVideo();
|
|
3077
|
+
for (const userID of this.requestedAudio) {
|
|
3078
|
+
void this.subscribeRemoteAudio(userID, true).catch((error) => {
|
|
3079
|
+
if (mediaActive()) this.listener?.onError?.(this.mapError(error));
|
|
3080
|
+
});
|
|
1851
3081
|
}
|
|
1852
|
-
|
|
3082
|
+
});
|
|
3083
|
+
engine.on(events.onNetworkQuality, (up, down) => {
|
|
3084
|
+
if (mediaActive()) this.listener?.onNetworkStatistics?.(Object.freeze({
|
|
3085
|
+
uplinkQuality: VeRtcStatistics.quality(up),
|
|
3086
|
+
downlinkQuality: VeRtcStatistics.quality(down)
|
|
3087
|
+
}));
|
|
3088
|
+
});
|
|
3089
|
+
engine.on(events.onLocalStreamStats, (stats) => {
|
|
3090
|
+
if (!mediaActive() || stats.isScreen || !this.videoRequested) return;
|
|
3091
|
+
const result = VeRtcStatistics.local(stats.videoStats);
|
|
3092
|
+
this.uplinkLossPercent = result.uplinkLossPercent;
|
|
3093
|
+
this.listener?.onLocalVideoStatistics?.(result);
|
|
3094
|
+
XmaxLogger.rtc.info(() => `VeRTC local video statistics: ${JSON.stringify(result)}`, 2 /* performance */);
|
|
3095
|
+
});
|
|
3096
|
+
engine.on(events.onRemoteStreamStats, (stats) => {
|
|
3097
|
+
if (!mediaActive() || stats.isScreen || !this.requestedVideo.has(stats.userId)) return;
|
|
3098
|
+
this.remoteStatistics.set(stats.userId, VeRtcStatistics.remote(stats.userId, stats.videoStats, this.uplinkLossPercent));
|
|
3099
|
+
this.emitRemoteStatistics();
|
|
1853
3100
|
});
|
|
1854
3101
|
}
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
3102
|
+
replayVideo() {
|
|
3103
|
+
for (const userID of this.publishedVideo) this.listener?.onRemoteVideoPublished(userID, true);
|
|
3104
|
+
}
|
|
3105
|
+
removeVideo(userID) {
|
|
3106
|
+
this.publishedVideo.delete(userID);
|
|
3107
|
+
this.remoteStatistics.delete(userID);
|
|
3108
|
+
this.emitRemoteStatistics();
|
|
3109
|
+
if (this.joined && !this.tokenExpired) this.listener?.onRemoteVideoPublished(userID, false);
|
|
3110
|
+
}
|
|
3111
|
+
emitRemoteStatistics() {
|
|
3112
|
+
this.listener?.onRemoteVideoStatistics?.(Object.freeze([...this.remoteStatistics.values()]));
|
|
3113
|
+
}
|
|
3114
|
+
clearStatistics() {
|
|
3115
|
+
this.remoteStatistics.clear();
|
|
3116
|
+
this.uplinkLossPercent = void 0;
|
|
3117
|
+
this.listener?.onLocalVideoStatistics?.(void 0);
|
|
3118
|
+
this.emitRemoteStatistics();
|
|
3119
|
+
}
|
|
3120
|
+
clearPlayer(userID) {
|
|
3121
|
+
this.audioSubscriptions.delete(userID);
|
|
3122
|
+
const player = this.audioPlayers.get(userID);
|
|
3123
|
+
if (player) {
|
|
3124
|
+
player.pause();
|
|
3125
|
+
player.srcObject = null;
|
|
3126
|
+
}
|
|
3127
|
+
this.audioPlayers.delete(userID);
|
|
3128
|
+
}
|
|
3129
|
+
clearPlayers() {
|
|
3130
|
+
this.audioSubscriptions.clear();
|
|
3131
|
+
for (const userID of this.audioPlayers.keys()) this.clearPlayer(userID);
|
|
3132
|
+
}
|
|
3133
|
+
clearRoom() {
|
|
3134
|
+
this.roomVersion++;
|
|
3135
|
+
this.roomAbort.abort();
|
|
3136
|
+
this.roomAbort = new AbortController();
|
|
3137
|
+
this.connection = void 0;
|
|
3138
|
+
this.joined = false;
|
|
3139
|
+
this.tokenExpired = false;
|
|
3140
|
+
this.publishPrivilegeExpired = false;
|
|
3141
|
+
this.subscribePrivilegeExpired = false;
|
|
3142
|
+
this.videoRequested = false;
|
|
3143
|
+
this.audioRequested = false;
|
|
3144
|
+
this.publishedVideo.clear();
|
|
3145
|
+
this.publishedAudio.clear();
|
|
3146
|
+
this.requestedVideo.clear();
|
|
3147
|
+
this.requestedAudio.clear();
|
|
3148
|
+
this.audioVolumes.clear();
|
|
3149
|
+
this.clearPlayers();
|
|
3150
|
+
this.clearStatistics();
|
|
3151
|
+
}
|
|
3152
|
+
/** 业务取消立即结束等待;底层迟到结果由版本检查清理。 */
|
|
3153
|
+
async cancellable(task, signal, timeoutMs) {
|
|
3154
|
+
const roomSignal = this.roomAbort.signal;
|
|
3155
|
+
return new Promise((resolve, reject) => {
|
|
3156
|
+
const timer = timeoutMs === void 0 ? void 0 : setTimeout(() => {
|
|
3157
|
+
cleanup();
|
|
3158
|
+
reject(new XmaxError("TIMEOUT" /* timeout */, "VeRTC room message send timed out"));
|
|
3159
|
+
}, timeoutMs);
|
|
3160
|
+
const cleanup = () => {
|
|
3161
|
+
clearTimeout(timer);
|
|
3162
|
+
roomSignal.removeEventListener("abort", cancel);
|
|
3163
|
+
signal?.removeEventListener("abort", cancel);
|
|
3164
|
+
};
|
|
3165
|
+
const cancel = () => {
|
|
3166
|
+
if (signal?.aborted && !roomSignal.aborted) this.roomVersion++;
|
|
3167
|
+
cleanup();
|
|
3168
|
+
reject(this.cancelled());
|
|
3169
|
+
};
|
|
3170
|
+
roomSignal.addEventListener("abort", cancel, { once: true });
|
|
3171
|
+
signal?.addEventListener("abort", cancel, { once: true });
|
|
3172
|
+
task.then((value) => {
|
|
3173
|
+
cleanup();
|
|
3174
|
+
resolve(value);
|
|
3175
|
+
}, (error) => {
|
|
3176
|
+
cleanup();
|
|
3177
|
+
reject(error);
|
|
3178
|
+
});
|
|
3179
|
+
if (roomSignal.aborted || signal?.aborted) cancel();
|
|
3180
|
+
});
|
|
3181
|
+
}
|
|
3182
|
+
isCurrent(engine, version) {
|
|
3183
|
+
return this.engine === engine && this.connection !== void 0 && this.roomVersion === version;
|
|
3184
|
+
}
|
|
3185
|
+
ensureCurrent(engine, version) {
|
|
3186
|
+
if (!this.isCurrent(engine, version)) throw this.cancelled();
|
|
3187
|
+
}
|
|
1858
3188
|
requireEngine() {
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
throw new XmaxError(
|
|
1862
|
-
"RTC_ERROR" /* rtcError */,
|
|
1863
|
-
"RTC engine is not initialized"
|
|
1864
|
-
);
|
|
1865
|
-
}
|
|
1866
|
-
return engine;
|
|
3189
|
+
if (!this.engine) throw new XmaxError("RTC_ERROR" /* rtcError */, "VeRTC is not initialized");
|
|
3190
|
+
return this.engine;
|
|
1867
3191
|
}
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
const message = typeof rtcError?.message === "string" && rtcError.message.trim().length > 0 ? rtcError.message.trim() : String(error);
|
|
1878
|
-
if (originName === "NotAllowedError" || originName === "SecurityError" || /NotAllowed/i.test(message)) {
|
|
1879
|
-
return new XmaxError("CAMERA_PERMISSION_DENIED" /* cameraPermissionDenied */, message);
|
|
1880
|
-
}
|
|
1881
|
-
if (originName) {
|
|
1882
|
-
return new XmaxError("MEDIA_ERROR" /* mediaError */, message);
|
|
3192
|
+
requireCamera() {
|
|
3193
|
+
if (!this.camera) throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Camera capture is not running");
|
|
3194
|
+
return this.camera;
|
|
3195
|
+
}
|
|
3196
|
+
async run(operation) {
|
|
3197
|
+
try {
|
|
3198
|
+
return await operation();
|
|
3199
|
+
} catch (error) {
|
|
3200
|
+
throw this.mapError(error);
|
|
1883
3201
|
}
|
|
1884
|
-
|
|
3202
|
+
}
|
|
3203
|
+
mapError(error, permissionCode) {
|
|
3204
|
+
if (error instanceof XmaxError) return error;
|
|
3205
|
+
const value = error;
|
|
3206
|
+
const code = typeof value?.code === "string" && /^[A-Z_0-9-]{1,64}$/.test(value.code) ? value.code : "UNKNOWN";
|
|
3207
|
+
const denied = value?.name === "NotAllowedError" || code === "PERMISSION_DENIED" || code === "DEVICE_PERMISSION_DENIED";
|
|
3208
|
+
return new XmaxError(permissionCode && denied ? permissionCode : "RTC_ERROR" /* rtcError */, `VeRTC operation failed (${code})`);
|
|
3209
|
+
}
|
|
3210
|
+
cancelled() {
|
|
3211
|
+
return new XmaxError("CANCELLED" /* cancelled */, "VeRTC operation was cancelled");
|
|
1885
3212
|
}
|
|
1886
3213
|
};
|
|
1887
3214
|
|
|
3215
|
+
// src/Foundation/RTC/RtcFactory.ts
|
|
3216
|
+
function createRtcManager(provider, environment) {
|
|
3217
|
+
switch (provider) {
|
|
3218
|
+
case "agora" /* agora */:
|
|
3219
|
+
return new AgoraRtcManager({ environment });
|
|
3220
|
+
case "vertc" /* vertc */:
|
|
3221
|
+
return new VeRtcManager();
|
|
3222
|
+
default:
|
|
3223
|
+
return new TrtcRtcManager();
|
|
3224
|
+
}
|
|
3225
|
+
}
|
|
3226
|
+
|
|
1888
3227
|
// src/Foundation/Media/Camera/CameraFrameValidator.ts
|
|
1889
3228
|
var CAMERA_WARMUP_MS = 200;
|
|
1890
3229
|
function waitForValidCameraFrame(track, signal) {
|
|
@@ -2134,7 +3473,7 @@ var _CameraController = class _CameraController {
|
|
|
2134
3473
|
__publicField(this, "storedUseMicrophone", false);
|
|
2135
3474
|
__publicField(this, "storedFrameValidationEnabled", true);
|
|
2136
3475
|
this.permissionManager = options?.permissionManager ?? new PermissionManager();
|
|
2137
|
-
this.rtcManager = options?.rtcManager ?? new
|
|
3476
|
+
this.rtcManager = options?.rtcManager ?? new TrtcRtcManager();
|
|
2138
3477
|
this.mediaService = options?.mediaService ?? new MediaService();
|
|
2139
3478
|
this.errorListener = options?.errorListener ?? (() => {
|
|
2140
3479
|
});
|
|
@@ -2425,6 +3764,53 @@ var RealtimeSession = class {
|
|
|
2425
3764
|
}
|
|
2426
3765
|
};
|
|
2427
3766
|
|
|
3767
|
+
// src/Foundation/RTC/RoomJoinConfiguration.ts
|
|
3768
|
+
var RoomJoinConfiguration = class {
|
|
3769
|
+
/**
|
|
3770
|
+
* 创建房间加入参数。
|
|
3771
|
+
*
|
|
3772
|
+
* @param init.roomID TRTC 房间号。
|
|
3773
|
+
* @param init.userID TRTC 登录身份。
|
|
3774
|
+
* @param init.sdkAppID TRTC SDKAppID(字符串形式)。
|
|
3775
|
+
* @param init.userSig TRTC 登录签名。
|
|
3776
|
+
* @param init.privateMapKey TRTC 进房鉴权字段。
|
|
3777
|
+
*/
|
|
3778
|
+
constructor(init) {
|
|
3779
|
+
/**
|
|
3780
|
+
* RTC 提供方。
|
|
3781
|
+
*/
|
|
3782
|
+
__publicField(this, "provider", "trtc");
|
|
3783
|
+
/**
|
|
3784
|
+
* 进房参数
|
|
3785
|
+
*/
|
|
3786
|
+
/**
|
|
3787
|
+
* TRTC 房间号(字符串房间号)。
|
|
3788
|
+
*/
|
|
3789
|
+
__publicField(this, "roomID");
|
|
3790
|
+
/**
|
|
3791
|
+
* TRTC 登录身份(`rtc_user_id`)。
|
|
3792
|
+
*/
|
|
3793
|
+
__publicField(this, "userID");
|
|
3794
|
+
/**
|
|
3795
|
+
* TRTC SDKAppID(字符串形式,进房时转换为数值)。
|
|
3796
|
+
*/
|
|
3797
|
+
__publicField(this, "sdkAppID");
|
|
3798
|
+
/**
|
|
3799
|
+
* TRTC 登录签名。
|
|
3800
|
+
*/
|
|
3801
|
+
__publicField(this, "userSig");
|
|
3802
|
+
/**
|
|
3803
|
+
* TRTC 进房鉴权字段(字符串房间号对应的 privateMapKey)。
|
|
3804
|
+
*/
|
|
3805
|
+
__publicField(this, "privateMapKey");
|
|
3806
|
+
this.roomID = init.roomID;
|
|
3807
|
+
this.userID = init.userID;
|
|
3808
|
+
this.sdkAppID = init.sdkAppID;
|
|
3809
|
+
this.userSig = init.userSig;
|
|
3810
|
+
this.privateMapKey = init.privateMapKey;
|
|
3811
|
+
}
|
|
3812
|
+
};
|
|
3813
|
+
|
|
2428
3814
|
// src/Service/Realtime/RealtimeSessionConnection.ts
|
|
2429
3815
|
var RealtimeSessionConnection = class {
|
|
2430
3816
|
/**
|
|
@@ -2443,7 +3829,7 @@ var RealtimeSessionConnection = class {
|
|
|
2443
3829
|
* RTC 提供方
|
|
2444
3830
|
*/
|
|
2445
3831
|
/**
|
|
2446
|
-
*
|
|
3832
|
+
* TRTC 凭据的提供方判别字段。
|
|
2447
3833
|
*/
|
|
2448
3834
|
__publicField(this, "provider");
|
|
2449
3835
|
/**
|
|
@@ -2485,6 +3871,12 @@ var RealtimeSessionConnection = class {
|
|
|
2485
3871
|
this.botID = init.botID;
|
|
2486
3872
|
}
|
|
2487
3873
|
};
|
|
3874
|
+
function toRoomJoinConfiguration(connection) {
|
|
3875
|
+
return connection.provider === "trtc" ? new RoomJoinConfiguration(connection) : connection;
|
|
3876
|
+
}
|
|
3877
|
+
function connectionAppID(connection) {
|
|
3878
|
+
return connection.provider === "trtc" ? connection.sdkAppID : connection.appID;
|
|
3879
|
+
}
|
|
2488
3880
|
|
|
2489
3881
|
// src/Service/Realtime/RealtimeSessionService.ts
|
|
2490
3882
|
var RealtimeSessionService = class _RealtimeSessionService {
|
|
@@ -2500,6 +3892,7 @@ var RealtimeSessionService = class _RealtimeSessionService {
|
|
|
2500
3892
|
* 服务层组件
|
|
2501
3893
|
*/
|
|
2502
3894
|
__publicField(this, "apiService");
|
|
3895
|
+
__publicField(this, "provider");
|
|
2503
3896
|
/**
|
|
2504
3897
|
* 心跳配置
|
|
2505
3898
|
*/
|
|
@@ -2509,7 +3902,16 @@ var RealtimeSessionService = class _RealtimeSessionService {
|
|
|
2509
3902
|
* 运行状态
|
|
2510
3903
|
*/
|
|
2511
3904
|
__publicField(this, "heartbeatVersion", 0);
|
|
3905
|
+
/**
|
|
3906
|
+
* 会话凭证缓存,支持心跳仅返回变更字段;关闭后清除。
|
|
3907
|
+
*/
|
|
3908
|
+
__publicField(this, "credentials", /* @__PURE__ */ new Map());
|
|
3909
|
+
__publicField(this, "refreshes", /* @__PURE__ */ new Map());
|
|
2512
3910
|
this.apiService = options.apiService;
|
|
3911
|
+
this.provider = options.provider ?? "trtc" /* trtc */;
|
|
3912
|
+
if (!Object.values(RtcProvider).includes(this.provider)) {
|
|
3913
|
+
throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Unsupported RTC provider");
|
|
3914
|
+
}
|
|
2513
3915
|
this.heartbeatIntervalMs = options.heartbeatIntervalMs ?? 1e4;
|
|
2514
3916
|
this.sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
2515
3917
|
}
|
|
@@ -2520,7 +3922,18 @@ var RealtimeSessionService = class _RealtimeSessionService {
|
|
|
2520
3922
|
const payload = await this.apiService.post("/session", {
|
|
2521
3923
|
model
|
|
2522
3924
|
});
|
|
2523
|
-
|
|
3925
|
+
try {
|
|
3926
|
+
const session = this.makeSession(payload, true);
|
|
3927
|
+
_RealtimeSessionService.ensureSessionActive(session);
|
|
3928
|
+
const credentials = _RealtimeSessionService.decodeConnectionPayload(payload.modelExtra);
|
|
3929
|
+
this.credentials.set(session.id, this.provider === "vertc" /* vertc */ ? { ...credentials, user_id: session.connection.userID } : credentials);
|
|
3930
|
+
return session;
|
|
3931
|
+
} catch (error) {
|
|
3932
|
+
const sessionID = _RealtimeSessionService.nonEmpty(payload.sessionUid);
|
|
3933
|
+
if (sessionID) await this.closeSession(sessionID).catch(() => {
|
|
3934
|
+
});
|
|
3935
|
+
throw error;
|
|
3936
|
+
}
|
|
2524
3937
|
}
|
|
2525
3938
|
/**
|
|
2526
3939
|
* 启动指定会话的周期心跳;重复启动会替换当前心跳。
|
|
@@ -2539,6 +3952,8 @@ var RealtimeSessionService = class _RealtimeSessionService {
|
|
|
2539
3952
|
* 关闭指定实时会话;请求带 keepalive,页面卸载后仍能发出。
|
|
2540
3953
|
*/
|
|
2541
3954
|
async closeSession(sessionID) {
|
|
3955
|
+
this.credentials.delete(sessionID);
|
|
3956
|
+
this.refreshes.delete(sessionID);
|
|
2542
3957
|
try {
|
|
2543
3958
|
await this.apiService.delete(
|
|
2544
3959
|
`/session/${sessionID}`,
|
|
@@ -2563,7 +3978,7 @@ var RealtimeSessionService = class _RealtimeSessionService {
|
|
|
2563
3978
|
return;
|
|
2564
3979
|
}
|
|
2565
3980
|
_RealtimeSessionService.ensureSessionActive(session);
|
|
2566
|
-
handlers.onRefresh?.(session);
|
|
3981
|
+
await handlers.onRefresh?.(session);
|
|
2567
3982
|
} catch (error) {
|
|
2568
3983
|
if (this.heartbeatVersion !== version) {
|
|
2569
3984
|
return;
|
|
@@ -2577,11 +3992,35 @@ var RealtimeSessionService = class _RealtimeSessionService {
|
|
|
2577
3992
|
/**
|
|
2578
3993
|
* 发送一次心跳请求并解析会话数据(不要求携带完整连接参数)。
|
|
2579
3994
|
*/
|
|
2580
|
-
|
|
3995
|
+
heartbeatSession(sessionID) {
|
|
3996
|
+
const pending = this.refreshes.get(sessionID);
|
|
3997
|
+
if (pending) return pending;
|
|
3998
|
+
const operation = this.performHeartbeat(sessionID);
|
|
3999
|
+
this.refreshes.set(sessionID, operation);
|
|
4000
|
+
void operation.finally(() => {
|
|
4001
|
+
if (this.refreshes.get(sessionID) === operation) this.refreshes.delete(sessionID);
|
|
4002
|
+
}).catch(() => {
|
|
4003
|
+
});
|
|
4004
|
+
return operation;
|
|
4005
|
+
}
|
|
4006
|
+
/**
|
|
4007
|
+
* 合并心跳部分凭据;已关闭会话的迟到响应不重建缓存。
|
|
4008
|
+
*/
|
|
4009
|
+
async performHeartbeat(sessionID) {
|
|
4010
|
+
const previous = this.credentials.get(sessionID);
|
|
2581
4011
|
const payload = await this.apiService.put(
|
|
2582
4012
|
`/session/${sessionID}/heartbeat`
|
|
2583
4013
|
);
|
|
2584
|
-
|
|
4014
|
+
const incoming = _RealtimeSessionService.decodeConnectionPayload(payload.modelExtra);
|
|
4015
|
+
if (payload.modelExtra != null && !incoming) {
|
|
4016
|
+
throw new XmaxError("SESSION_ERROR" /* sessionError */, "Invalid heartbeat RTC credentials");
|
|
4017
|
+
}
|
|
4018
|
+
const merged = incoming ? { ...previous, ...incoming } : previous;
|
|
4019
|
+
const session = this.makeSession({ ...payload, modelExtra: merged }, merged !== void 0);
|
|
4020
|
+
if (session.id !== sessionID) throw new XmaxError("SESSION_ERROR" /* sessionError */, "Heartbeat session identity changed");
|
|
4021
|
+
_RealtimeSessionService.ensureSessionActive(session);
|
|
4022
|
+
if (previous && this.credentials.get(sessionID) === previous && merged) this.credentials.set(sessionID, merged);
|
|
4023
|
+
return session;
|
|
2585
4024
|
}
|
|
2586
4025
|
/**
|
|
2587
4026
|
* 校验会话仍处于活跃状态;状态明确非 ACTIVE 时抛出会话错误。
|
|
@@ -2598,12 +4037,12 @@ var RealtimeSessionService = class _RealtimeSessionService {
|
|
|
2598
4037
|
/**
|
|
2599
4038
|
* 由会话接口数据构造会话模型;`requiresConnection` 要求携带完整 RTC 连接参数。
|
|
2600
4039
|
*/
|
|
2601
|
-
|
|
4040
|
+
makeSession(payload, requiresConnection) {
|
|
2602
4041
|
const sessionID = _RealtimeSessionService.nonEmpty(payload.sessionUid);
|
|
2603
4042
|
if (!sessionID) {
|
|
2604
4043
|
throw new XmaxError("SESSION_ERROR" /* sessionError */, "Invalid session response");
|
|
2605
4044
|
}
|
|
2606
|
-
const connection =
|
|
4045
|
+
const connection = this.makeConnection(payload.modelExtra, payload.userUid);
|
|
2607
4046
|
if (requiresConnection && !connection) {
|
|
2608
4047
|
throw new XmaxError(
|
|
2609
4048
|
"SESSION_ERROR" /* sessionError */,
|
|
@@ -2619,19 +4058,30 @@ var RealtimeSessionService = class _RealtimeSessionService {
|
|
|
2619
4058
|
});
|
|
2620
4059
|
}
|
|
2621
4060
|
/**
|
|
2622
|
-
* 解析 `modelExtra` 中的
|
|
4061
|
+
* 解析 `modelExtra` 中的 RTC 连接参数;字段不完整或提供方不支持时返回空。
|
|
2623
4062
|
*/
|
|
2624
|
-
|
|
4063
|
+
makeConnection(modelExtra, fallbackUserID) {
|
|
2625
4064
|
const payload = _RealtimeSessionService.decodeConnectionPayload(modelExtra);
|
|
2626
4065
|
if (!payload) {
|
|
2627
4066
|
return void 0;
|
|
2628
4067
|
}
|
|
2629
|
-
|
|
2630
|
-
return void 0;
|
|
2631
|
-
}
|
|
4068
|
+
const provider = this.provider;
|
|
2632
4069
|
const roomID = _RealtimeSessionService.nonEmpty(payload.room_id);
|
|
2633
4070
|
const sdkAppID = _RealtimeSessionService.nonEmpty(payload.rtc_app_id);
|
|
2634
|
-
const userID = _RealtimeSessionService.nonEmpty(payload.rtc_user_id);
|
|
4071
|
+
const userID = provider === "vertc" /* vertc */ ? _RealtimeSessionService.nonEmpty(payload.user_id) ?? _RealtimeSessionService.nonEmpty(fallbackUserID) : _RealtimeSessionService.nonEmpty(payload.rtc_user_id);
|
|
4072
|
+
if (!roomID || !sdkAppID || !userID) return void 0;
|
|
4073
|
+
if (provider === "agora" /* agora */ || provider === "vertc" /* vertc */) {
|
|
4074
|
+
const roomToken = _RealtimeSessionService.nonEmpty(payload.room_token);
|
|
4075
|
+
if (!roomToken) return void 0;
|
|
4076
|
+
return Object.freeze({
|
|
4077
|
+
provider,
|
|
4078
|
+
roomID,
|
|
4079
|
+
appID: sdkAppID,
|
|
4080
|
+
userID,
|
|
4081
|
+
roomToken,
|
|
4082
|
+
botID: _RealtimeSessionService.nonEmpty(provider === "vertc" /* vertc */ ? payload.bot_name : payload.rtc_bot_id)
|
|
4083
|
+
});
|
|
4084
|
+
}
|
|
2635
4085
|
const userSig = _RealtimeSessionService.nonEmpty(payload.user_sig);
|
|
2636
4086
|
const privateMapKey = _RealtimeSessionService.nonEmpty(
|
|
2637
4087
|
payload.private_map_key_with_string_room_id
|
|
@@ -2655,12 +4105,12 @@ var RealtimeSessionService = class _RealtimeSessionService {
|
|
|
2655
4105
|
static decodeConnectionPayload(modelExtra) {
|
|
2656
4106
|
if (typeof modelExtra === "string") {
|
|
2657
4107
|
try {
|
|
2658
|
-
return JSON.parse(modelExtra);
|
|
4108
|
+
return _RealtimeSessionService.decodeConnectionPayload(JSON.parse(modelExtra));
|
|
2659
4109
|
} catch {
|
|
2660
4110
|
return void 0;
|
|
2661
4111
|
}
|
|
2662
4112
|
}
|
|
2663
|
-
if (typeof modelExtra === "object" && modelExtra !== null) {
|
|
4113
|
+
if (typeof modelExtra === "object" && modelExtra !== null && !Array.isArray(modelExtra)) {
|
|
2664
4114
|
return modelExtra;
|
|
2665
4115
|
}
|
|
2666
4116
|
return void 0;
|
|
@@ -2669,7 +4119,7 @@ var RealtimeSessionService = class _RealtimeSessionService {
|
|
|
2669
4119
|
* 归一化可选字符串:去除首尾空白后为空时返回 `undefined`。
|
|
2670
4120
|
*/
|
|
2671
4121
|
static nonEmpty(value) {
|
|
2672
|
-
const normalized = value
|
|
4122
|
+
const normalized = typeof value === "string" ? value.trim() : void 0;
|
|
2673
4123
|
return normalized || void 0;
|
|
2674
4124
|
}
|
|
2675
4125
|
};
|
|
@@ -2776,49 +4226,6 @@ var RemoteStream = class {
|
|
|
2776
4226
|
}
|
|
2777
4227
|
};
|
|
2778
4228
|
|
|
2779
|
-
// src/Foundation/RTC/RoomJoinConfiguration.ts
|
|
2780
|
-
var RoomJoinConfiguration = class {
|
|
2781
|
-
/**
|
|
2782
|
-
* 创建房间加入参数。
|
|
2783
|
-
*
|
|
2784
|
-
* @param init.roomID TRTC 房间号。
|
|
2785
|
-
* @param init.userID TRTC 登录身份。
|
|
2786
|
-
* @param init.sdkAppID TRTC SDKAppID(字符串形式)。
|
|
2787
|
-
* @param init.userSig TRTC 登录签名。
|
|
2788
|
-
* @param init.privateMapKey TRTC 进房鉴权字段。
|
|
2789
|
-
*/
|
|
2790
|
-
constructor(init) {
|
|
2791
|
-
/**
|
|
2792
|
-
* 进房参数
|
|
2793
|
-
*/
|
|
2794
|
-
/**
|
|
2795
|
-
* TRTC 房间号(字符串房间号)。
|
|
2796
|
-
*/
|
|
2797
|
-
__publicField(this, "roomID");
|
|
2798
|
-
/**
|
|
2799
|
-
* TRTC 登录身份(`rtc_user_id`)。
|
|
2800
|
-
*/
|
|
2801
|
-
__publicField(this, "userID");
|
|
2802
|
-
/**
|
|
2803
|
-
* TRTC SDKAppID(字符串形式,进房时转换为数值)。
|
|
2804
|
-
*/
|
|
2805
|
-
__publicField(this, "sdkAppID");
|
|
2806
|
-
/**
|
|
2807
|
-
* TRTC 登录签名。
|
|
2808
|
-
*/
|
|
2809
|
-
__publicField(this, "userSig");
|
|
2810
|
-
/**
|
|
2811
|
-
* TRTC 进房鉴权字段(字符串房间号对应的 privateMapKey)。
|
|
2812
|
-
*/
|
|
2813
|
-
__publicField(this, "privateMapKey");
|
|
2814
|
-
this.roomID = init.roomID;
|
|
2815
|
-
this.userID = init.userID;
|
|
2816
|
-
this.sdkAppID = init.sdkAppID;
|
|
2817
|
-
this.userSig = init.userSig;
|
|
2818
|
-
this.privateMapKey = init.privateMapKey;
|
|
2819
|
-
}
|
|
2820
|
-
};
|
|
2821
|
-
|
|
2822
4229
|
// src/Stream/Room/RoomEvent.ts
|
|
2823
4230
|
var RoomEvent = class _RoomEvent {
|
|
2824
4231
|
/**
|
|
@@ -2969,7 +4376,7 @@ var RoomHeartbeat = class {
|
|
|
2969
4376
|
return;
|
|
2970
4377
|
}
|
|
2971
4378
|
try {
|
|
2972
|
-
this.rtcManager.sendRoomMessage(RoomEvent.heartbeat({ userID }));
|
|
4379
|
+
await this.rtcManager.sendRoomMessage(RoomEvent.heartbeat({ userID }));
|
|
2973
4380
|
} catch (error) {
|
|
2974
4381
|
if (this.cycleVersion !== version) {
|
|
2975
4382
|
return;
|
|
@@ -2986,7 +4393,11 @@ var RoomHeartbeat = class {
|
|
|
2986
4393
|
// src/Stream/Room/RoomMessageCodec.ts
|
|
2987
4394
|
var CHUNK_EVENT = "__trtc_chunk__";
|
|
2988
4395
|
var _RoomMessageCodec = class _RoomMessageCodec {
|
|
2989
|
-
|
|
4396
|
+
/**
|
|
4397
|
+
* 创建厂商协议编解码器;TRTC 保持原事件名,Agora 使用 __agora_chunk__;null 表示不拆包。
|
|
4398
|
+
*/
|
|
4399
|
+
constructor(chunkEvent = CHUNK_EVENT) {
|
|
4400
|
+
__publicField(this, "chunkEvent", chunkEvent);
|
|
2990
4401
|
/**
|
|
2991
4402
|
* 接收缓存
|
|
2992
4403
|
*/
|
|
@@ -2996,10 +4407,10 @@ var _RoomMessageCodec = class _RoomMessageCodec {
|
|
|
2996
4407
|
* 编码出站消息。
|
|
2997
4408
|
*
|
|
2998
4409
|
* @returns 不超过阈值时返回仅含原消息的单元素数组;
|
|
2999
|
-
*
|
|
4410
|
+
* 否则返回按当前提供方分片协议包装的 JSON 数组。
|
|
3000
4411
|
*/
|
|
3001
4412
|
encodeOutgoing(message) {
|
|
3002
|
-
if (_RoomMessageCodec.byteLength(message) <= _RoomMessageCodec.chunkThresholdBytes) {
|
|
4413
|
+
if (this.chunkEvent === null || _RoomMessageCodec.byteLength(message) <= _RoomMessageCodec.chunkThresholdBytes) {
|
|
3003
4414
|
return [message];
|
|
3004
4415
|
}
|
|
3005
4416
|
const eventId = _RoomMessageCodec.makeEventId();
|
|
@@ -3014,7 +4425,7 @@ var _RoomMessageCodec = class _RoomMessageCodec {
|
|
|
3014
4425
|
);
|
|
3015
4426
|
return slices.map(
|
|
3016
4427
|
(data, index) => JSON.stringify({
|
|
3017
|
-
event:
|
|
4428
|
+
event: this.chunkEvent,
|
|
3018
4429
|
eventId,
|
|
3019
4430
|
index,
|
|
3020
4431
|
count,
|
|
@@ -3036,7 +4447,7 @@ var _RoomMessageCodec = class _RoomMessageCodec {
|
|
|
3036
4447
|
XmaxLogger.room.warning(() => "\u623F\u95F4\u6D88\u606F\u4E0D\u662F\u5408\u6CD5 JSON (Room Message Is Not Valid JSON)");
|
|
3037
4448
|
return void 0;
|
|
3038
4449
|
}
|
|
3039
|
-
const chunk =
|
|
4450
|
+
const chunk = this.parseChunk(parsed);
|
|
3040
4451
|
if (!chunk) {
|
|
3041
4452
|
return parsed;
|
|
3042
4453
|
}
|
|
@@ -3080,12 +4491,12 @@ var _RoomMessageCodec = class _RoomMessageCodec {
|
|
|
3080
4491
|
/**
|
|
3081
4492
|
* 从已解析的值识别分片消息;字段不完整时按普通业务消息处理。
|
|
3082
4493
|
*/
|
|
3083
|
-
|
|
3084
|
-
if (typeof parsed !== "object" || parsed === null) {
|
|
4494
|
+
parseChunk(parsed) {
|
|
4495
|
+
if (this.chunkEvent === null || typeof parsed !== "object" || parsed === null) {
|
|
3085
4496
|
return void 0;
|
|
3086
4497
|
}
|
|
3087
4498
|
const record = parsed;
|
|
3088
|
-
if (record.event !==
|
|
4499
|
+
if (record.event !== this.chunkEvent || typeof record.eventId !== "string" || record.eventId.length === 0 || !Number.isInteger(record.index) || !Number.isInteger(record.count) || typeof record.data !== "string") {
|
|
3089
4500
|
return void 0;
|
|
3090
4501
|
}
|
|
3091
4502
|
const index = record.index;
|
|
@@ -3115,7 +4526,7 @@ var _RoomMessageCodec = class _RoomMessageCodec {
|
|
|
3115
4526
|
let current = "";
|
|
3116
4527
|
let currentBytes = 0;
|
|
3117
4528
|
for (const char of message) {
|
|
3118
|
-
const bytes = _RoomMessageCodec.byteLength(char);
|
|
4529
|
+
const bytes = _RoomMessageCodec.byteLength(JSON.stringify(char)) - 2;
|
|
3119
4530
|
if (currentBytes + bytes > budget && current.length > 0) {
|
|
3120
4531
|
slices.push(current);
|
|
3121
4532
|
current = "";
|
|
@@ -3184,6 +4595,8 @@ var RoomController = class {
|
|
|
3184
4595
|
*/
|
|
3185
4596
|
__publicField(this, "heartbeat");
|
|
3186
4597
|
__publicField(this, "codec");
|
|
4598
|
+
__publicField(this, "customCodec");
|
|
4599
|
+
__publicField(this, "sendTail", Promise.resolve());
|
|
3187
4600
|
/**
|
|
3188
4601
|
* 事件监听
|
|
3189
4602
|
*/
|
|
@@ -3196,6 +4609,7 @@ var RoomController = class {
|
|
|
3196
4609
|
this.rtcManager = options.rtcManager;
|
|
3197
4610
|
this.heartbeat = options.heartbeat ?? new RoomHeartbeat({ rtcManager: options.rtcManager });
|
|
3198
4611
|
this.codec = options.codec ?? new RoomMessageCodec();
|
|
4612
|
+
this.customCodec = options.codec !== void 0;
|
|
3199
4613
|
}
|
|
3200
4614
|
/**
|
|
3201
4615
|
* 加入实时房间,并在异步边界前后确认连接操作仍然有效。
|
|
@@ -3218,15 +4632,10 @@ var RoomController = class {
|
|
|
3218
4632
|
this.state = { kind: "joining", operationID };
|
|
3219
4633
|
this.heartbeat.stop();
|
|
3220
4634
|
try {
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
roomID: connection.roomID,
|
|
3224
|
-
userID: connection.userID,
|
|
3225
|
-
sdkAppID: connection.sdkAppID,
|
|
3226
|
-
userSig: connection.userSig,
|
|
3227
|
-
privateMapKey: connection.privateMapKey
|
|
3228
|
-
})
|
|
4635
|
+
if (!this.customCodec) this.codec = new RoomMessageCodec(
|
|
4636
|
+
connection.provider === "vertc" ? null : connection.provider === "agora" ? "__agora_chunk__" : "__trtc_chunk__"
|
|
3229
4637
|
);
|
|
4638
|
+
await this.rtcManager.joinRoom(toRoomJoinConfiguration(connection));
|
|
3230
4639
|
ensureActive();
|
|
3231
4640
|
if (this.state.kind !== "joining" || this.state.operationID !== operationID || this.leaveOperation) {
|
|
3232
4641
|
throw new XmaxError("CANCELLED" /* cancelled */, "RTC room join was cancelled");
|
|
@@ -3266,7 +4675,7 @@ var RoomController = class {
|
|
|
3266
4675
|
* @throws 房间未就绪或信令发送失败时抛出错误。
|
|
3267
4676
|
*/
|
|
3268
4677
|
startGeneration(options) {
|
|
3269
|
-
this.send(
|
|
4678
|
+
return this.send(
|
|
3270
4679
|
RoomEvent.start({
|
|
3271
4680
|
userID: this.requireUserID(),
|
|
3272
4681
|
taskID: options.taskID,
|
|
@@ -3282,7 +4691,7 @@ var RoomController = class {
|
|
|
3282
4691
|
* @throws 房间未就绪或信令发送失败时抛出错误。
|
|
3283
4692
|
*/
|
|
3284
4693
|
changeGenerationCondition(options) {
|
|
3285
|
-
this.send(
|
|
4694
|
+
return this.send(
|
|
3286
4695
|
RoomEvent.changeCondition({
|
|
3287
4696
|
userID: this.requireUserID(),
|
|
3288
4697
|
taskID: options.taskID,
|
|
@@ -3299,7 +4708,7 @@ var RoomController = class {
|
|
|
3299
4708
|
*/
|
|
3300
4709
|
changeTargetSize(options) {
|
|
3301
4710
|
options.ensureActive();
|
|
3302
|
-
this.send(
|
|
4711
|
+
return this.send(
|
|
3303
4712
|
RoomEvent.changeTargetSize({
|
|
3304
4713
|
userID: this.requireUserID(),
|
|
3305
4714
|
taskID: options.taskID,
|
|
@@ -3312,18 +4721,18 @@ var RoomController = class {
|
|
|
3312
4721
|
*/
|
|
3313
4722
|
stopGeneration(taskID) {
|
|
3314
4723
|
if (!taskID || this.state.kind !== "joined") {
|
|
3315
|
-
return;
|
|
4724
|
+
return Promise.resolve();
|
|
3316
4725
|
}
|
|
3317
|
-
this.send(RoomEvent.stop({ userID: this.state.userID, taskID }));
|
|
4726
|
+
return this.send(RoomEvent.stop({ userID: this.state.userID, taskID }));
|
|
3318
4727
|
}
|
|
3319
4728
|
/**
|
|
3320
4729
|
* 发送生成任务的交互轨迹;任务标识或轨迹为空时忽略。
|
|
3321
4730
|
*/
|
|
3322
4731
|
sendTracks(taskID, points) {
|
|
3323
4732
|
if (!taskID || points.length === 0) {
|
|
3324
|
-
return;
|
|
4733
|
+
return Promise.resolve();
|
|
3325
4734
|
}
|
|
3326
|
-
this.send(
|
|
4735
|
+
return this.send(
|
|
3327
4736
|
RoomEvent.tracks({
|
|
3328
4737
|
userID: this.requireUserID(),
|
|
3329
4738
|
taskID,
|
|
@@ -3344,8 +4753,12 @@ var RoomController = class {
|
|
|
3344
4753
|
this.state = { kind: "idle" };
|
|
3345
4754
|
this.heartbeat.stop();
|
|
3346
4755
|
this.codec.reset();
|
|
3347
|
-
|
|
3348
|
-
|
|
4756
|
+
this.sendTail = Promise.resolve();
|
|
4757
|
+
try {
|
|
4758
|
+
await this.rtcManager.leaveRoom();
|
|
4759
|
+
} finally {
|
|
4760
|
+
this.leaveOperation = void 0;
|
|
4761
|
+
}
|
|
3349
4762
|
}
|
|
3350
4763
|
/**
|
|
3351
4764
|
* 组包入站消息并按目标用户过滤后分发完整业务消息。
|
|
@@ -3367,18 +4780,26 @@ var RoomController = class {
|
|
|
3367
4780
|
* 发送房间信令并输出日志。
|
|
3368
4781
|
*/
|
|
3369
4782
|
send(message) {
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
4783
|
+
const state2 = this.state;
|
|
4784
|
+
const packets = this.codec.encodeOutgoing(message);
|
|
4785
|
+
const operation = this.sendTail.then(async () => {
|
|
4786
|
+
for (const packet of packets) {
|
|
4787
|
+
if (state2 !== this.state || state2.kind !== "joined") {
|
|
4788
|
+
throw new XmaxError("CANCELLED" /* cancelled */, "Room message was cancelled");
|
|
4789
|
+
}
|
|
4790
|
+
await this.rtcManager.sendRoomMessage(packet);
|
|
3375
4791
|
}
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
() => `\u53D1\u9001\u623F\u95F4\u4FE1\u4EE4 (Outbound Room Signaling)
|
|
4792
|
+
XmaxLogger.room.info(
|
|
4793
|
+
() => `\u53D1\u9001\u623F\u95F4\u4FE1\u4EE4 (Outbound Room Signaling)
|
|
3379
4794
|
\u2514\u2500 ${XmaxLogger.localized("\u5185\u5BB9\uFF1A", "Content:")}
|
|
3380
4795
|
${JSON.stringify(JSON.parse(message), null, 2)}`
|
|
3381
|
-
|
|
4796
|
+
);
|
|
4797
|
+
}).catch((error) => {
|
|
4798
|
+
throw XmaxError.from(error);
|
|
4799
|
+
});
|
|
4800
|
+
this.sendTail = operation.catch(() => {
|
|
4801
|
+
});
|
|
4802
|
+
return operation;
|
|
3382
4803
|
}
|
|
3383
4804
|
/**
|
|
3384
4805
|
* 返回当前房间中的用户标识;未进房时抛出错误。
|
|
@@ -3551,6 +4972,9 @@ function makeInitialState() {
|
|
|
3551
4972
|
localVideoPublished: false,
|
|
3552
4973
|
localAudioPublished: false,
|
|
3553
4974
|
subscribedRemoteUserIDs: /* @__PURE__ */ new Set(),
|
|
4975
|
+
publishedRemoteUserIDs: /* @__PURE__ */ new Set(),
|
|
4976
|
+
remoteVideoTracks: /* @__PURE__ */ new Map(),
|
|
4977
|
+
remoteVideoSubscriptions: /* @__PURE__ */ new Map(),
|
|
3554
4978
|
subscribedRemoteAudioUserIDs: /* @__PURE__ */ new Set()
|
|
3555
4979
|
};
|
|
3556
4980
|
}
|
|
@@ -3604,6 +5028,10 @@ var StreamController = class {
|
|
|
3604
5028
|
});
|
|
3605
5029
|
this.generationTimeoutMs = options.generationTimeoutMs ?? 3e4;
|
|
3606
5030
|
this.rtcManager.setEventListener({
|
|
5031
|
+
onTokenWillExpire: options.onCredentialsRequired,
|
|
5032
|
+
onTokenExpired: options.onCredentialsRequired,
|
|
5033
|
+
onRoomRejoining: () => this.handleRoomRejoining(),
|
|
5034
|
+
onError: (error) => this.errorListener(error),
|
|
3607
5035
|
onNetworkStatistics: (statistics) => {
|
|
3608
5036
|
if (this.state.localVideoPublished) {
|
|
3609
5037
|
this.networkStatisticsListener?.(statistics);
|
|
@@ -3682,9 +5110,9 @@ var StreamController = class {
|
|
|
3682
5110
|
* @throws 连接已取消,或 RTC 进房、房间配置与本地流发布失败时抛出错误。
|
|
3683
5111
|
*/
|
|
3684
5112
|
async connect(connection, includeLocalAudio, ensureActive, beforePublish) {
|
|
5113
|
+
this.configureRoom({ roomID: connection.roomID, botID: connection.botID });
|
|
3685
5114
|
await this.roomController.join(connection, ensureActive);
|
|
3686
5115
|
ensureActive();
|
|
3687
|
-
this.configureRoom({ roomID: connection.roomID, botID: connection.botID });
|
|
3688
5116
|
XmaxLogger.stream.info(
|
|
3689
5117
|
() => `RTC \u623F\u95F4\u5DF2\u914D\u7F6E (RTC Room Configured)
|
|
3690
5118
|
\u2514\u2500 roomID: ${connection.roomID}, botID: ${connection.botID ?? "(\u672A\u8BBE\u7F6E)"}`
|
|
@@ -3700,12 +5128,18 @@ var StreamController = class {
|
|
|
3700
5128
|
await this.resetStream();
|
|
3701
5129
|
await this.roomController.leave();
|
|
3702
5130
|
}
|
|
5131
|
+
/**
|
|
5132
|
+
* 将会话刷新后的凭据应用到当前 RTC 连接。
|
|
5133
|
+
*/
|
|
5134
|
+
async updateCredentials(connection, signal) {
|
|
5135
|
+
await this.rtcManager.updateCredentials(toRoomJoinConfiguration(connection), signal);
|
|
5136
|
+
}
|
|
3703
5137
|
/**
|
|
3704
5138
|
* 建立生成任务并发送开始信令。
|
|
3705
5139
|
*
|
|
3706
5140
|
* @returns 等待远端结果流确认的 Promise。
|
|
3707
|
-
* @throws 任务标识无效、RTC
|
|
3708
|
-
*
|
|
5141
|
+
* @throws 任务标识无效、RTC 房间未就绪或已有生成任务时同步抛错;
|
|
5142
|
+
* 发送失败或确认超时通过返回的 Promise 拒绝。
|
|
3709
5143
|
*/
|
|
3710
5144
|
beginGeneration(options) {
|
|
3711
5145
|
const taskID = options.taskID.trim();
|
|
@@ -3747,24 +5181,44 @@ var StreamController = class {
|
|
|
3747
5181
|
};
|
|
3748
5182
|
this.state.generationTaskID = taskID;
|
|
3749
5183
|
this.state.generationWaiter = waiter;
|
|
5184
|
+
void confirmation.catch(() => {
|
|
5185
|
+
});
|
|
3750
5186
|
try {
|
|
3751
|
-
this.roomController.startGeneration({
|
|
5187
|
+
const sending = this.roomController.startGeneration({
|
|
3752
5188
|
taskID,
|
|
3753
5189
|
videoFormat: options.videoFormat,
|
|
3754
5190
|
targetSize: options.targetSize,
|
|
3755
5191
|
context: options.context
|
|
3756
5192
|
});
|
|
5193
|
+
const state2 = this.state;
|
|
5194
|
+
const sent = Promise.resolve(sending).then(() => {
|
|
5195
|
+
if (this.state !== state2 || state2.generationTaskID !== taskID) return;
|
|
5196
|
+
for (const userID of state2.publishedRemoteUserIDs) {
|
|
5197
|
+
void this.handleRemoteVideoPublished(userID, true);
|
|
5198
|
+
}
|
|
5199
|
+
}).catch((error) => {
|
|
5200
|
+
const mapped = XmaxError.from(error);
|
|
5201
|
+
this.rejectGenerationStart(taskID, mapped);
|
|
5202
|
+
if (this.state.generationTaskID === taskID) {
|
|
5203
|
+
this.state.generationTaskID = void 0;
|
|
5204
|
+
void this.roomController.stopGeneration(taskID).catch(() => {
|
|
5205
|
+
});
|
|
5206
|
+
}
|
|
5207
|
+
throw mapped;
|
|
5208
|
+
});
|
|
5209
|
+
return Promise.all([sent, confirmation]).then(() => {
|
|
5210
|
+
});
|
|
3757
5211
|
} catch (error) {
|
|
3758
5212
|
const mapped = XmaxError.from(error);
|
|
3759
|
-
this.state.generationTaskID = void 0;
|
|
3760
5213
|
this.rejectGenerationStart(taskID, mapped);
|
|
5214
|
+
this.state.generationTaskID = void 0;
|
|
3761
5215
|
try {
|
|
3762
|
-
this.roomController.stopGeneration(taskID)
|
|
5216
|
+
void this.roomController.stopGeneration(taskID).catch(() => {
|
|
5217
|
+
});
|
|
3763
5218
|
} catch {
|
|
3764
5219
|
}
|
|
3765
5220
|
throw mapped;
|
|
3766
5221
|
}
|
|
3767
|
-
return confirmation;
|
|
3768
5222
|
}
|
|
3769
5223
|
/**
|
|
3770
5224
|
* 在远端首帧已经可显示后订阅当前生成流的音频。
|
|
@@ -3786,9 +5240,9 @@ var StreamController = class {
|
|
|
3786
5240
|
*
|
|
3787
5241
|
* @throws RTC 房间未就绪或条件变更信令发送失败时抛出错误。
|
|
3788
5242
|
*/
|
|
3789
|
-
updateGeneration(options) {
|
|
5243
|
+
async updateGeneration(options) {
|
|
3790
5244
|
try {
|
|
3791
|
-
this.roomController.changeGenerationCondition({
|
|
5245
|
+
await this.roomController.changeGenerationCondition({
|
|
3792
5246
|
taskID: options.taskID,
|
|
3793
5247
|
videoFormat: options.videoFormat,
|
|
3794
5248
|
targetSize: options.targetSize,
|
|
@@ -3803,9 +5257,9 @@ var StreamController = class {
|
|
|
3803
5257
|
*
|
|
3804
5258
|
* @throws 操作已取消、房间未就绪或信令发送失败时抛出错误。
|
|
3805
5259
|
*/
|
|
3806
|
-
changeTargetSize(taskID, targetSize, ensureActive) {
|
|
5260
|
+
async changeTargetSize(taskID, targetSize, ensureActive) {
|
|
3807
5261
|
try {
|
|
3808
|
-
this.roomController.changeTargetSize({ taskID, targetSize, ensureActive });
|
|
5262
|
+
await this.roomController.changeTargetSize({ taskID, targetSize, ensureActive });
|
|
3809
5263
|
} catch (error) {
|
|
3810
5264
|
throw XmaxError.from(error);
|
|
3811
5265
|
}
|
|
@@ -3821,7 +5275,7 @@ var StreamController = class {
|
|
|
3821
5275
|
return;
|
|
3822
5276
|
}
|
|
3823
5277
|
try {
|
|
3824
|
-
this.roomController.stopGeneration(stoppedTaskID);
|
|
5278
|
+
await this.roomController.stopGeneration(stoppedTaskID);
|
|
3825
5279
|
} catch (error) {
|
|
3826
5280
|
throw XmaxError.from(error);
|
|
3827
5281
|
}
|
|
@@ -3831,9 +5285,9 @@ var StreamController = class {
|
|
|
3831
5285
|
*
|
|
3832
5286
|
* @throws RTC 房间未就绪或轨迹信令发送失败时抛出错误。
|
|
3833
5287
|
*/
|
|
3834
|
-
sendTracks(taskID, points) {
|
|
5288
|
+
async sendTracks(taskID, points) {
|
|
3835
5289
|
try {
|
|
3836
|
-
this.roomController.sendTracks(taskID, points);
|
|
5290
|
+
await this.roomController.sendTracks(taskID, points);
|
|
3837
5291
|
} catch (error) {
|
|
3838
5292
|
throw XmaxError.from(error);
|
|
3839
5293
|
}
|
|
@@ -3908,6 +5362,12 @@ var StreamController = class {
|
|
|
3908
5362
|
if (!this.state.roomID) {
|
|
3909
5363
|
return;
|
|
3910
5364
|
}
|
|
5365
|
+
if (published) this.state.publishedRemoteUserIDs.add(trimmedUserID);
|
|
5366
|
+
else {
|
|
5367
|
+
this.state.publishedRemoteUserIDs.delete(trimmedUserID);
|
|
5368
|
+
this.state.remoteVideoSubscriptions.delete(trimmedUserID);
|
|
5369
|
+
this.state.remoteVideoTracks.delete(trimmedUserID);
|
|
5370
|
+
}
|
|
3911
5371
|
if (this.state.botID && this.state.botID !== trimmedUserID) {
|
|
3912
5372
|
if (this.state.generationTaskID === void 0) {
|
|
3913
5373
|
XmaxLogger.stream.warning(
|
|
@@ -3932,31 +5392,49 @@ var StreamController = class {
|
|
|
3932
5392
|
this.clearRemoteStream();
|
|
3933
5393
|
}
|
|
3934
5394
|
}
|
|
5395
|
+
/**
|
|
5396
|
+
* RTC 重进房时废弃旧媒体绑定;业务任务、音频订阅意图和生成确认等待器继续保留。
|
|
5397
|
+
*/
|
|
5398
|
+
handleRoomRejoining() {
|
|
5399
|
+
this.state.publishedRemoteUserIDs.clear();
|
|
5400
|
+
this.state.subscribedRemoteUserIDs.clear();
|
|
5401
|
+
this.state.remoteVideoSubscriptions.clear();
|
|
5402
|
+
this.state.remoteVideoTracks.clear();
|
|
5403
|
+
this.state.activeRemoteStream = void 0;
|
|
5404
|
+
this.clearRemoteStream();
|
|
5405
|
+
}
|
|
3935
5406
|
/**
|
|
3936
5407
|
* 订阅远端视频并在成功后就绪或确认生成任务。
|
|
3937
5408
|
*/
|
|
3938
5409
|
async subscribeRemoteVideo(userID) {
|
|
3939
|
-
|
|
5410
|
+
const state2 = this.state;
|
|
5411
|
+
const existing = state2.remoteVideoTracks.get(userID);
|
|
5412
|
+
if (existing) {
|
|
5413
|
+
if (state2.generationWaiter) this.confirmOrUpdateRemoteStream(userID, existing);
|
|
3940
5414
|
return;
|
|
3941
5415
|
}
|
|
5416
|
+
if (state2.remoteVideoSubscriptions.has(userID)) return;
|
|
5417
|
+
const subscription = {};
|
|
5418
|
+
state2.remoteVideoSubscriptions.set(userID, subscription);
|
|
5419
|
+
const isCurrent = () => this.state === state2 && state2.remoteVideoSubscriptions.get(userID) === subscription;
|
|
3942
5420
|
try {
|
|
3943
5421
|
const track = await this.rtcManager.subscribeRemoteVideo(userID, true);
|
|
5422
|
+
if (!isCurrent()) return;
|
|
3944
5423
|
if (!track) {
|
|
3945
5424
|
throw new XmaxError(
|
|
3946
5425
|
"MEDIA_ERROR" /* mediaError */,
|
|
3947
5426
|
"Remote video track is not available"
|
|
3948
5427
|
);
|
|
3949
5428
|
}
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
}
|
|
3953
|
-
this.state.subscribedRemoteUserIDs.add(userID);
|
|
5429
|
+
state2.subscribedRemoteUserIDs.add(userID);
|
|
5430
|
+
state2.remoteVideoTracks.set(userID, track);
|
|
3954
5431
|
XmaxLogger.stream.info(
|
|
3955
5432
|
() => `\u8FDC\u7AEF\u89C6\u9891\u8BA2\u9605\u6210\u529F (Remote Video Subscribed)
|
|
3956
5433
|
\u2514\u2500 userID: ${userID}`
|
|
3957
5434
|
);
|
|
3958
5435
|
this.confirmOrUpdateRemoteStream(userID, track);
|
|
3959
5436
|
} catch (error) {
|
|
5437
|
+
if (!isCurrent()) return;
|
|
3960
5438
|
const mapped = XmaxError.from(error);
|
|
3961
5439
|
const pendingTaskID = this.state.generationWaiter ? this.state.generationTaskID : void 0;
|
|
3962
5440
|
if (pendingTaskID) {
|
|
@@ -3964,6 +5442,8 @@ var StreamController = class {
|
|
|
3964
5442
|
} else {
|
|
3965
5443
|
this.errorListener(mapped);
|
|
3966
5444
|
}
|
|
5445
|
+
} finally {
|
|
5446
|
+
if (isCurrent()) state2.remoteVideoSubscriptions.delete(userID);
|
|
3967
5447
|
}
|
|
3968
5448
|
}
|
|
3969
5449
|
/**
|
|
@@ -4680,6 +6160,10 @@ var XmaxRealtimeConnectionManager = class {
|
|
|
4680
6160
|
* 会话资源
|
|
4681
6161
|
*/
|
|
4682
6162
|
__publicField(this, "activeSession");
|
|
6163
|
+
__publicField(this, "refreshVersion", 0);
|
|
6164
|
+
__publicField(this, "credentialRefresh");
|
|
6165
|
+
__publicField(this, "credentialApplication", Promise.resolve());
|
|
6166
|
+
__publicField(this, "credentialController");
|
|
4683
6167
|
/**
|
|
4684
6168
|
* 远端轨道与渲染绑定
|
|
4685
6169
|
*/
|
|
@@ -4719,6 +6203,11 @@ var XmaxRealtimeConnectionManager = class {
|
|
|
4719
6203
|
"Session does not contain RTC join information"
|
|
4720
6204
|
);
|
|
4721
6205
|
}
|
|
6206
|
+
if (connection.provider !== (this.dependencies.provider ?? "trtc" /* trtc */)) {
|
|
6207
|
+
throw new XmaxError("SESSION_ERROR" /* sessionError */, "RTC adapter configuration does not match the session service");
|
|
6208
|
+
}
|
|
6209
|
+
const refreshVersion = ++this.refreshVersion;
|
|
6210
|
+
this.credentialController = new AbortController();
|
|
4722
6211
|
const videoFormat = options.localTrack.videoFormat;
|
|
4723
6212
|
if (!videoFormat) {
|
|
4724
6213
|
throw new XmaxError(
|
|
@@ -4745,12 +6234,12 @@ var XmaxRealtimeConnectionManager = class {
|
|
|
4745
6234
|
this.dependencies.onHeartbeatFailure(sessionID, error);
|
|
4746
6235
|
},
|
|
4747
6236
|
onRefresh: (refreshed) => {
|
|
4748
|
-
this.handleSessionRefresh(refreshed);
|
|
6237
|
+
return this.handleSessionRefresh(refreshed, refreshVersion);
|
|
4749
6238
|
}
|
|
4750
6239
|
});
|
|
4751
6240
|
const remoteTrack = new RealtimeVideoTrack({
|
|
4752
6241
|
id: connection.botID ?? "video-remote",
|
|
4753
|
-
videoFormat: options.localTrack.videoFormat
|
|
6242
|
+
videoFormat: options.remoteVideoFormat ?? options.localTrack.videoFormat
|
|
4754
6243
|
});
|
|
4755
6244
|
this.activeRemoteTrack = remoteTrack;
|
|
4756
6245
|
this.registerRemoteBinding(remoteTrack);
|
|
@@ -4773,8 +6262,38 @@ var XmaxRealtimeConnectionManager = class {
|
|
|
4773
6262
|
* 停止会话心跳;未配置会话服务时不执行操作。
|
|
4774
6263
|
*/
|
|
4775
6264
|
stopHeartbeat() {
|
|
6265
|
+
this.refreshVersion++;
|
|
6266
|
+
this.credentialController?.abort();
|
|
6267
|
+
this.credentialController = void 0;
|
|
6268
|
+
this.credentialRefresh = void 0;
|
|
6269
|
+
this.credentialApplication = Promise.resolve();
|
|
4776
6270
|
this.dependencies.sessionService?.stopHeartbeat();
|
|
4777
6271
|
}
|
|
6272
|
+
/**
|
|
6273
|
+
* RTC Token 过期时复用会话心跳获取新凭据,并合并同一实例的并发请求。
|
|
6274
|
+
*/
|
|
6275
|
+
async refreshCredentials() {
|
|
6276
|
+
if (this.credentialRefresh) return this.credentialRefresh;
|
|
6277
|
+
const current = this.activeSession;
|
|
6278
|
+
if (!current?.connection || current.connection.provider === "trtc") return;
|
|
6279
|
+
const version = this.refreshVersion;
|
|
6280
|
+
const operation = (async () => {
|
|
6281
|
+
try {
|
|
6282
|
+
const session = await this.requireSessionService().heartbeatSession(current.id);
|
|
6283
|
+
await this.handleSessionRefresh(session, version, true);
|
|
6284
|
+
} catch (error) {
|
|
6285
|
+
if (version === this.refreshVersion && this.activeSession?.id === current.id) {
|
|
6286
|
+
this.dependencies.onHeartbeatFailure(current.id, XmaxError.from(error));
|
|
6287
|
+
}
|
|
6288
|
+
}
|
|
6289
|
+
})();
|
|
6290
|
+
this.credentialRefresh = operation;
|
|
6291
|
+
try {
|
|
6292
|
+
await operation;
|
|
6293
|
+
} finally {
|
|
6294
|
+
if (this.credentialRefresh === operation) this.credentialRefresh = void 0;
|
|
6295
|
+
}
|
|
6296
|
+
}
|
|
4778
6297
|
/**
|
|
4779
6298
|
* 等待接收轨解除静音;超时只警告,取消必须立即退出并移除监听。
|
|
4780
6299
|
*/
|
|
@@ -4882,9 +6401,18 @@ var XmaxRealtimeConnectionManager = class {
|
|
|
4882
6401
|
* 心跳成功后的凭据刷新:仅凭据变化时覆盖本地缓存;房间绑定信息
|
|
4883
6402
|
* (提供方、房间、应用、登录身份)变化时当前连接失效,按失败结束。
|
|
4884
6403
|
*/
|
|
4885
|
-
handleSessionRefresh(session) {
|
|
6404
|
+
handleSessionRefresh(session, version, force = false) {
|
|
6405
|
+
const operation = this.credentialApplication.then(() => this.applySessionRefresh(session, version, force));
|
|
6406
|
+
this.credentialApplication = operation.catch(() => {
|
|
6407
|
+
});
|
|
6408
|
+
return operation;
|
|
6409
|
+
}
|
|
6410
|
+
/**
|
|
6411
|
+
* 校验绑定并应用最新凭据,异步刷新期间失效的房间不得恢复缓存。
|
|
6412
|
+
*/
|
|
6413
|
+
async applySessionRefresh(session, version, force) {
|
|
4886
6414
|
const current = this.activeSession;
|
|
4887
|
-
if (!current || current.id !== session.id) {
|
|
6415
|
+
if (version !== this.refreshVersion || !current || current.id !== session.id) {
|
|
4888
6416
|
return;
|
|
4889
6417
|
}
|
|
4890
6418
|
const next = session.connection;
|
|
@@ -4892,7 +6420,7 @@ var XmaxRealtimeConnectionManager = class {
|
|
|
4892
6420
|
return;
|
|
4893
6421
|
}
|
|
4894
6422
|
const previous = current.connection;
|
|
4895
|
-
const bindingChanged = previous !== void 0 && (next.provider !== previous.provider || next.roomID !== previous.roomID || next.
|
|
6423
|
+
const bindingChanged = previous !== void 0 && (next.provider !== previous.provider || next.roomID !== previous.roomID || connectionAppID(next) !== connectionAppID(previous) || next.botID !== previous.botID || next.userID !== previous.userID);
|
|
4896
6424
|
if (bindingChanged) {
|
|
4897
6425
|
this.dependencies.onHeartbeatFailure(
|
|
4898
6426
|
session.id,
|
|
@@ -4900,6 +6428,10 @@ var XmaxRealtimeConnectionManager = class {
|
|
|
4900
6428
|
);
|
|
4901
6429
|
return;
|
|
4902
6430
|
}
|
|
6431
|
+
if (next.provider !== "trtc" && (force || !previous || previous.provider === "trtc" || next.roomToken !== previous.roomToken)) {
|
|
6432
|
+
await this.dependencies.streamController.updateCredentials(next, this.credentialController?.signal);
|
|
6433
|
+
if (version !== this.refreshVersion || this.activeSession !== current) return;
|
|
6434
|
+
}
|
|
4903
6435
|
this.activeSession = new RealtimeSession({
|
|
4904
6436
|
id: session.id,
|
|
4905
6437
|
userID: session.userID ?? current.userID,
|
|
@@ -5012,16 +6544,17 @@ var XmaxRealtimeGenerationManager = class _XmaxRealtimeGenerationManager {
|
|
|
5012
6544
|
/**
|
|
5013
6545
|
* 更新活动任务的生成条件;信令发送成功后才覆盖缓存,无任务或上下文时抛错。
|
|
5014
6546
|
*/
|
|
5015
|
-
update(taskID, options) {
|
|
6547
|
+
async update(taskID, options) {
|
|
5016
6548
|
const context = options.context ?? this.currentContext;
|
|
5017
6549
|
if (!taskID || !context) {
|
|
5018
6550
|
throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "A realtime context is required to update the current generation");
|
|
5019
6551
|
}
|
|
5020
|
-
this.streamController.updateGeneration({
|
|
6552
|
+
await this.streamController.updateGeneration({
|
|
5021
6553
|
taskID,
|
|
5022
6554
|
videoFormat: options.videoFormat,
|
|
5023
6555
|
context
|
|
5024
6556
|
});
|
|
6557
|
+
if (this.activeTaskID !== taskID) throw RealtimeCoordinator.cancelledError();
|
|
5025
6558
|
this.currentContext = context;
|
|
5026
6559
|
}
|
|
5027
6560
|
/**
|
|
@@ -5109,6 +6642,10 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
5109
6642
|
*/
|
|
5110
6643
|
__publicField(this, "options");
|
|
5111
6644
|
__publicField(this, "mediaService");
|
|
6645
|
+
/**
|
|
6646
|
+
* 本地流创建时的生成格式;上行编码调整不改变模型输入配置与远端插帧尺寸。
|
|
6647
|
+
*/
|
|
6648
|
+
__publicField(this, "generationVideoFormat");
|
|
5112
6649
|
/**
|
|
5113
6650
|
* 插帧能力、用户配置与渲染版本
|
|
5114
6651
|
*/
|
|
@@ -5157,7 +6694,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
5157
6694
|
this.interpolationRequested = options.isFrameInterpolationEnabled;
|
|
5158
6695
|
this.supportsInterpolation = dependencies?.supportsFrameInterpolation ?? (async (size) => await frameInterpolationAdapter(size) !== null);
|
|
5159
6696
|
this.errorHandler = new RealtimeErrorHandler();
|
|
5160
|
-
const rtcManager = dependencies?.rtcManager ??
|
|
6697
|
+
const rtcManager = dependencies?.rtcManager ?? createRtcManager(options.provider, dependencies?.environment ?? "china" /* china */);
|
|
5161
6698
|
this.cameraController = dependencies?.cameraController ?? new CameraController({
|
|
5162
6699
|
rtcManager,
|
|
5163
6700
|
mediaService: this.mediaService,
|
|
@@ -5165,9 +6702,12 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
5165
6702
|
void this.errorHandler.report(error);
|
|
5166
6703
|
}
|
|
5167
6704
|
});
|
|
5168
|
-
const sessionService = dependencies?.sessionService ?? (dependencies?.apiService ? new RealtimeSessionService({ apiService: dependencies.apiService }) : void 0);
|
|
6705
|
+
const sessionService = dependencies?.sessionService ?? (dependencies?.apiService ? new RealtimeSessionService({ apiService: dependencies.apiService, provider: options.provider }) : void 0);
|
|
5169
6706
|
this.streamController = dependencies?.streamController ?? new StreamController({
|
|
5170
6707
|
rtcManager,
|
|
6708
|
+
onCredentialsRequired: () => {
|
|
6709
|
+
void this.connectionManager.refreshCredentials();
|
|
6710
|
+
},
|
|
5171
6711
|
errorListener: (error) => {
|
|
5172
6712
|
void this.errorHandler.report(error);
|
|
5173
6713
|
},
|
|
@@ -5177,6 +6717,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
5177
6717
|
}
|
|
5178
6718
|
});
|
|
5179
6719
|
this.connectionManager = new XmaxRealtimeConnectionManager({
|
|
6720
|
+
provider: options.provider,
|
|
5180
6721
|
sessionService,
|
|
5181
6722
|
streamController: this.streamController,
|
|
5182
6723
|
timing: this.launchTimer,
|
|
@@ -5232,7 +6773,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
5232
6773
|
*/
|
|
5233
6774
|
async setFrameInterpolationEnabled(enabled) {
|
|
5234
6775
|
await this.coordinator.run("configuration" /* configuration */, void 0, async (token) => {
|
|
5235
|
-
const format = this.cameraController.currentTrack?.videoFormat;
|
|
6776
|
+
const format = this.generationVideoFormat ?? this.cameraController.currentTrack?.videoFormat;
|
|
5236
6777
|
const size = format && enabled ? this.mediaService.resolveFrameInterpolationSize(format) : void 0;
|
|
5237
6778
|
if (enabled && !await this.checkInterpolationSupport(size, token)) {
|
|
5238
6779
|
token.ensureCurrent();
|
|
@@ -5485,6 +7026,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
5485
7026
|
throw error;
|
|
5486
7027
|
}
|
|
5487
7028
|
token.ensureCurrent();
|
|
7029
|
+
this.generationVideoFormat = stream.videoTrack?.videoFormat;
|
|
5488
7030
|
completeCamera();
|
|
5489
7031
|
this.cameraController.setPreviewReadyHandler((isCurrent) => {
|
|
5490
7032
|
void this.coordinator.localPreviewDidBecomeReady(isCurrent);
|
|
@@ -5500,6 +7042,26 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
5500
7042
|
this.permissionWatchDispose?.();
|
|
5501
7043
|
this.permissionWatchDispose = void 0;
|
|
5502
7044
|
}
|
|
7045
|
+
/**
|
|
7046
|
+
* 应用完整的上行视频格式,成功后更新本地轨道元数据,不改变生成格式或生命周期。
|
|
7047
|
+
* 复用配置操作准入,避免与建连、切换摄像头或其他配置更新并发。
|
|
7048
|
+
*
|
|
7049
|
+
* @throws 无本地相机流、参数无效、操作冲突或底层编码更新失败时抛错。
|
|
7050
|
+
*/
|
|
7051
|
+
async updateVideoFormat(videoFormat) {
|
|
7052
|
+
await this.coordinator.run("configuration" /* configuration */, void 0, async (token) => {
|
|
7053
|
+
const track = this.cameraController.currentTrack;
|
|
7054
|
+
const previousFormat = track?.videoFormat;
|
|
7055
|
+
if (!track || !previousFormat) {
|
|
7056
|
+
throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Local camera stream is not started");
|
|
7057
|
+
}
|
|
7058
|
+
videoFormat.validate();
|
|
7059
|
+
await this.streamController.setVideoEncoderConfig(videoFormat);
|
|
7060
|
+
token.ensureCurrent();
|
|
7061
|
+
this.generationVideoFormat ?? (this.generationVideoFormat = previousFormat);
|
|
7062
|
+
track.updateVideoFormat(videoFormat);
|
|
7063
|
+
});
|
|
7064
|
+
}
|
|
5503
7065
|
/**
|
|
5504
7066
|
* 停止本地相机流并释放本地预览与 RTC 资源。
|
|
5505
7067
|
*/
|
|
@@ -5513,6 +7075,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
5513
7075
|
this.disposePermissionWatch();
|
|
5514
7076
|
await this.cameraController.stopLocalCameraStream();
|
|
5515
7077
|
token.ensureCurrent();
|
|
7078
|
+
this.generationVideoFormat = void 0;
|
|
5516
7079
|
await this.coordinator.commit(
|
|
5517
7080
|
new RealtimeState({ connectionState: "Idle" /* idle */ }),
|
|
5518
7081
|
token
|
|
@@ -5592,7 +7155,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
5592
7155
|
"The local stream must be created and started by this realtime manager"
|
|
5593
7156
|
);
|
|
5594
7157
|
}
|
|
5595
|
-
const videoFormat = localTrack.videoFormat;
|
|
7158
|
+
const videoFormat = this.generationVideoFormat ?? localTrack.videoFormat;
|
|
5596
7159
|
if (!videoFormat) {
|
|
5597
7160
|
throw new XmaxError(
|
|
5598
7161
|
"INVALID_CONFIGURATION" /* invalidConfiguration */,
|
|
@@ -5601,10 +7164,11 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
5601
7164
|
}
|
|
5602
7165
|
const currentState = this.coordinator.currentState;
|
|
5603
7166
|
if (currentState.connectionState === "Generating" /* generating */) {
|
|
5604
|
-
this.generationManager.update(currentState.taskID, {
|
|
7167
|
+
await this.generationManager.update(currentState.taskID, {
|
|
5605
7168
|
videoFormat,
|
|
5606
7169
|
context: options.context
|
|
5607
7170
|
});
|
|
7171
|
+
token.ensureCurrent();
|
|
5608
7172
|
return this.connectionManager.makeRemoteStream();
|
|
5609
7173
|
}
|
|
5610
7174
|
const context = this.generationManager.validateContext(options.context);
|
|
@@ -5669,6 +7233,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
5669
7233
|
}
|
|
5670
7234
|
const remote = await this.connectionManager.connect({
|
|
5671
7235
|
localTrack,
|
|
7236
|
+
remoteVideoFormat: this.generationVideoFormat,
|
|
5672
7237
|
model: this.options.model,
|
|
5673
7238
|
includeLocalAudio: this.cameraController.useMicrophone,
|
|
5674
7239
|
ensureCurrent: () => token.ensureCurrent(),
|
|
@@ -5744,6 +7309,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
5744
7309
|
await this.generationManager.reset(taskID);
|
|
5745
7310
|
const sessionID = await this.connectionManager.disconnect();
|
|
5746
7311
|
if (scope === 1 /* all */) {
|
|
7312
|
+
this.generationVideoFormat = void 0;
|
|
5747
7313
|
this.disposePermissionWatch();
|
|
5748
7314
|
try {
|
|
5749
7315
|
await this.cameraController.stopLocalCameraStream();
|
|
@@ -5850,12 +7416,18 @@ var XmaxClient = class {
|
|
|
5850
7416
|
}
|
|
5851
7417
|
/**
|
|
5852
7418
|
* 创建实时媒体 Manager。
|
|
7419
|
+
* 模型专用地址优先于环境默认地址,并用于该 Manager 的整个会话生命周期。
|
|
5853
7420
|
*
|
|
5854
7421
|
* @param options 实时生成模型等业务配置。
|
|
5855
7422
|
* @returns 可用于本地相机预览、实时连接与生成的实时 Manager。
|
|
5856
7423
|
*/
|
|
5857
7424
|
createRealtimeManager(options) {
|
|
5858
|
-
|
|
7425
|
+
const baseURL = modelBaseURL(options.model);
|
|
7426
|
+
const apiService = baseURL ? new ApiService({ apiKey: this.configuration.apiKey, baseURL }) : this.apiService;
|
|
7427
|
+
return new XmaxRealtimeManager(options, {
|
|
7428
|
+
apiService,
|
|
7429
|
+
environment: this.configuration.environment
|
|
7430
|
+
});
|
|
5859
7431
|
}
|
|
5860
7432
|
/**
|
|
5861
7433
|
* 创建媒体处理与能力查询 Service。
|
|
@@ -5881,10 +7453,15 @@ var RealtimeConfiguration = class {
|
|
|
5881
7453
|
* 创建实时业务配置。
|
|
5882
7454
|
*
|
|
5883
7455
|
* @param init.model 实时生成业务使用的模型。
|
|
7456
|
+
* @param init.provider RTC 提供方,默认 TRTC;创建后不可切换。
|
|
5884
7457
|
* @param init.isFrameInterpolationEnabled 是否默认开启远端生成画面的插帧。
|
|
5885
7458
|
* @param init.frameInterpolation 插帧开关和显示帧率上限的详细配置。
|
|
5886
7459
|
*/
|
|
5887
7460
|
constructor(init) {
|
|
7461
|
+
/**
|
|
7462
|
+
* RTC 配置
|
|
7463
|
+
*/
|
|
7464
|
+
__publicField(this, "provider");
|
|
5888
7465
|
/**
|
|
5889
7466
|
* 模型配置
|
|
5890
7467
|
*/
|
|
@@ -5903,6 +7480,10 @@ var RealtimeConfiguration = class {
|
|
|
5903
7480
|
* 已补全默认值的只读插帧配置。
|
|
5904
7481
|
*/
|
|
5905
7482
|
__publicField(this, "frameInterpolation");
|
|
7483
|
+
this.provider = init.provider ?? "trtc" /* trtc */;
|
|
7484
|
+
if (!Object.values(RtcProvider).includes(this.provider)) {
|
|
7485
|
+
throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Unsupported RTC provider");
|
|
7486
|
+
}
|
|
5906
7487
|
this.model = init.model;
|
|
5907
7488
|
const targetFrameRate = init.frameInterpolation?.targetFrameRate ?? 60;
|
|
5908
7489
|
if (!Number.isFinite(targetFrameRate) || targetFrameRate < 1 || targetFrameRate > 60) {
|
|
@@ -8750,6 +10331,7 @@ export {
|
|
|
8750
10331
|
StoredFile,
|
|
8751
10332
|
StorageService,
|
|
8752
10333
|
CameraPosition,
|
|
10334
|
+
RtcProvider,
|
|
8753
10335
|
RealtimeVideoTrack,
|
|
8754
10336
|
RealtimeMediaStream,
|
|
8755
10337
|
RealtimeConnectionState,
|
|
@@ -8763,4 +10345,4 @@ export {
|
|
|
8763
10345
|
XmaxVideoView,
|
|
8764
10346
|
XmaxRealtimeVideoView
|
|
8765
10347
|
};
|
|
8766
|
-
//# sourceMappingURL=chunk-
|
|
10348
|
+
//# sourceMappingURL=chunk-FMAQXPTE.js.map
|