@xmaxai/web-sdk 1.0.2 → 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-5NIEZJP5.cjs → chunk-FEL5MJMJ.cjs} +2060 -430
- package/dist/chunk-FEL5MJMJ.cjs.map +1 -0
- package/dist/{chunk-6RIEDVRQ.js → chunk-FMAQXPTE.js} +1964 -334
- 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 +74 -21
- package/dist/index.d.ts +74 -21
- 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-5NIEZJP5.cjs.map +0 -1
- package/dist/chunk-6RIEDVRQ.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 字段。
|
|
@@ -263,10 +263,12 @@ var _ApiService = class _ApiService {
|
|
|
263
263
|
}
|
|
264
264
|
/**
|
|
265
265
|
* 发送请求并返回统一响应中的数据。
|
|
266
|
+
*
|
|
267
|
+
* @param options.keepalive 为 true 时请求可存活于页面卸载之后。
|
|
266
268
|
*/
|
|
267
|
-
async request(method, path, body) {
|
|
269
|
+
async request(method, path, body, options) {
|
|
268
270
|
this.validateConfiguration();
|
|
269
|
-
const request = this.makeRequest(method, path, body);
|
|
271
|
+
const request = this.makeRequest(method, path, body, options);
|
|
270
272
|
const startedAt = Date.now();
|
|
271
273
|
let status;
|
|
272
274
|
let text;
|
|
@@ -313,8 +315,8 @@ var _ApiService = class _ApiService {
|
|
|
313
315
|
/**
|
|
314
316
|
* 发送 DELETE 请求。
|
|
315
317
|
*/
|
|
316
|
-
delete(path) {
|
|
317
|
-
return this.request("DELETE" /* delete */, path);
|
|
318
|
+
delete(path, options) {
|
|
319
|
+
return this.request("DELETE" /* delete */, path, void 0, options);
|
|
318
320
|
}
|
|
319
321
|
/**
|
|
320
322
|
* 校验 API Key 与 Base URL 配置。
|
|
@@ -344,7 +346,7 @@ var _ApiService = class _ApiService {
|
|
|
344
346
|
/**
|
|
345
347
|
* 构造请求地址、请求头与请求体;路径必须相对且与 Base URL 同源。
|
|
346
348
|
*/
|
|
347
|
-
makeRequest(method, path, body) {
|
|
349
|
+
makeRequest(method, path, body, options) {
|
|
348
350
|
const normalizedPath = path.trim();
|
|
349
351
|
if (normalizedPath.length === 0 || normalizedPath.includes("://") || normalizedPath.startsWith("//")) {
|
|
350
352
|
throw new XmaxError(
|
|
@@ -385,7 +387,8 @@ var _ApiService = class _ApiService {
|
|
|
385
387
|
"X-SDK-Version": XMAX_SDK_VERSION
|
|
386
388
|
},
|
|
387
389
|
body: encodedBody,
|
|
388
|
-
signal: AbortSignal.timeout(this.timeoutMs)
|
|
390
|
+
signal: AbortSignal.timeout(this.timeoutMs),
|
|
391
|
+
...options?.keepalive ? { keepalive: true } : {}
|
|
389
392
|
}
|
|
390
393
|
};
|
|
391
394
|
}
|
|
@@ -518,22 +521,22 @@ var RealtimeVideoFormat = class _RealtimeVideoFormat {
|
|
|
518
521
|
* @throws 尺寸、帧率或码率配置无效时抛出错误。
|
|
519
522
|
*/
|
|
520
523
|
validate() {
|
|
521
|
-
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) {
|
|
522
525
|
throw new XmaxError(
|
|
523
526
|
"INVALID_CONFIGURATION" /* invalidConfiguration */,
|
|
524
527
|
"Realtime video width and height must be positive even numbers, and fps must be greater than zero"
|
|
525
528
|
);
|
|
526
529
|
}
|
|
527
|
-
if (this.minimumBitrate !== void 0 && this.minimumBitrate < 0) {
|
|
530
|
+
if (this.minimumBitrate !== void 0 && (!Number.isFinite(this.minimumBitrate) || this.minimumBitrate < 0)) {
|
|
528
531
|
throw new XmaxError(
|
|
529
532
|
"INVALID_CONFIGURATION" /* invalidConfiguration */,
|
|
530
|
-
"Minimum bitrate must not
|
|
533
|
+
"Minimum bitrate must be finite and not negative"
|
|
531
534
|
);
|
|
532
535
|
}
|
|
533
|
-
if (this.maximumBitrate !== void 0 && this.maximumBitrate <= 0) {
|
|
536
|
+
if (this.maximumBitrate !== void 0 && (!Number.isFinite(this.maximumBitrate) || this.maximumBitrate <= 0)) {
|
|
534
537
|
throw new XmaxError(
|
|
535
538
|
"INVALID_CONFIGURATION" /* invalidConfiguration */,
|
|
536
|
-
"Maximum bitrate must be greater than zero"
|
|
539
|
+
"Maximum bitrate must be finite and greater than zero"
|
|
537
540
|
);
|
|
538
541
|
}
|
|
539
542
|
if (this.minimumBitrate !== void 0 && this.maximumBitrate !== void 0 && this.minimumBitrate > this.maximumBitrate) {
|
|
@@ -561,26 +564,39 @@ var RealtimeVideoFormat = class _RealtimeVideoFormat {
|
|
|
561
564
|
// src/Service/Realtime/RealtimeModel.ts
|
|
562
565
|
var RealtimeModel = /* @__PURE__ */ ((RealtimeModel2) => {
|
|
563
566
|
RealtimeModel2["x2_0"] = "x2.0";
|
|
564
|
-
RealtimeModel2["x2_0_pro"] = "x2.0-pro";
|
|
565
567
|
RealtimeModel2["x2_0_trtc"] = "x2.0-trtc";
|
|
568
|
+
RealtimeModel2["x2_0_agora"] = "x2.0-agora";
|
|
569
|
+
RealtimeModel2["x2_1_preview"] = "x2.1-preview";
|
|
566
570
|
return RealtimeModel2;
|
|
567
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
|
+
};
|
|
568
576
|
var RESOLUTION_BUCKETS = {
|
|
569
|
-
["x2.
|
|
570
|
-
|
|
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 */]: [
|
|
571
582
|
{ width: 1024, height: 1920 },
|
|
572
583
|
{ width: 1920, height: 1024 }
|
|
573
584
|
],
|
|
585
|
+
["x2.0" /* x2_0 */]: [],
|
|
574
586
|
["x2.0-trtc" /* x2_0_trtc */]: [
|
|
575
587
|
{ width: 1024, height: 1920 },
|
|
576
588
|
{ width: 1920, height: 1024 }
|
|
577
589
|
]
|
|
578
590
|
};
|
|
579
591
|
var MAXIMUM_INPUT_PIXELS = {
|
|
592
|
+
["x2.1-preview" /* x2_1_preview */]: 21e5,
|
|
593
|
+
["x2.0-agora" /* x2_0_agora */]: 21e5,
|
|
580
594
|
["x2.0" /* x2_0 */]: 128e4,
|
|
581
|
-
["x2.0-pro" /* x2_0_pro */]: 21e5,
|
|
582
595
|
["x2.0-trtc" /* x2_0_trtc */]: 21e5
|
|
583
596
|
};
|
|
597
|
+
function modelBaseURL(model) {
|
|
598
|
+
return MODEL_BASE_URLS[model];
|
|
599
|
+
}
|
|
584
600
|
function resolutionBuckets(model) {
|
|
585
601
|
return RESOLUTION_BUCKETS[model];
|
|
586
602
|
}
|
|
@@ -1006,7 +1022,780 @@ var CameraPosition = /* @__PURE__ */ ((CameraPosition3) => {
|
|
|
1006
1022
|
return CameraPosition3;
|
|
1007
1023
|
})(CameraPosition || {});
|
|
1008
1024
|
|
|
1009
|
-
// src/Foundation/
|
|
1025
|
+
// src/Foundation/Permissions/MediaPermissionWatch.ts
|
|
1026
|
+
var PERMISSION_POLL_INTERVAL_MS = 100;
|
|
1027
|
+
async function watchMediaPermissionPrompt(includeMicrophone) {
|
|
1028
|
+
if (typeof navigator === "undefined" || !navigator.permissions?.query) {
|
|
1029
|
+
return void 0;
|
|
1030
|
+
}
|
|
1031
|
+
const names = ["camera", ...includeMicrophone ? ["microphone"] : []];
|
|
1032
|
+
const queryOnce = () => Promise.all(
|
|
1033
|
+
names.map(async (name) => {
|
|
1034
|
+
const status = await navigator.permissions.query({
|
|
1035
|
+
name
|
|
1036
|
+
});
|
|
1037
|
+
return status.state;
|
|
1038
|
+
})
|
|
1039
|
+
);
|
|
1040
|
+
const queryStates = () => Promise.all(
|
|
1041
|
+
names.map(async (name) => {
|
|
1042
|
+
try {
|
|
1043
|
+
const status = await navigator.permissions.query({
|
|
1044
|
+
name
|
|
1045
|
+
});
|
|
1046
|
+
return status.state;
|
|
1047
|
+
} catch {
|
|
1048
|
+
return "prompt";
|
|
1049
|
+
}
|
|
1050
|
+
})
|
|
1051
|
+
);
|
|
1052
|
+
let states;
|
|
1053
|
+
try {
|
|
1054
|
+
states = await queryOnce();
|
|
1055
|
+
} catch {
|
|
1056
|
+
return void 0;
|
|
1057
|
+
}
|
|
1058
|
+
if (states.every((state2) => state2 === "granted")) {
|
|
1059
|
+
return { decided: Promise.resolve(), dispose() {
|
|
1060
|
+
} };
|
|
1061
|
+
}
|
|
1062
|
+
if (!states.includes("prompt")) {
|
|
1063
|
+
return void 0;
|
|
1064
|
+
}
|
|
1065
|
+
let disposed = false;
|
|
1066
|
+
let resolveDecided;
|
|
1067
|
+
const decided = new Promise((resolve) => {
|
|
1068
|
+
resolveDecided = resolve;
|
|
1069
|
+
});
|
|
1070
|
+
const finishIfDecided = (current) => {
|
|
1071
|
+
if (disposed || current.includes("prompt")) {
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
dispose();
|
|
1075
|
+
resolveDecided();
|
|
1076
|
+
};
|
|
1077
|
+
const poll = setInterval(() => {
|
|
1078
|
+
void queryStates().then(finishIfDecided);
|
|
1079
|
+
}, PERMISSION_POLL_INTERVAL_MS);
|
|
1080
|
+
function dispose() {
|
|
1081
|
+
disposed = true;
|
|
1082
|
+
clearInterval(poll);
|
|
1083
|
+
}
|
|
1084
|
+
return { decided, dispose };
|
|
1085
|
+
}
|
|
1086
|
+
|
|
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
|
|
1010
1799
|
var RtcEngineLease = class {
|
|
1011
1800
|
/**
|
|
1012
1801
|
* 为已创建的引擎生成独占租约标识,供引擎管理器核对释放请求。
|
|
@@ -1239,7 +2028,15 @@ var RtcStatsLogger = class {
|
|
|
1239
2028
|
}
|
|
1240
2029
|
};
|
|
1241
2030
|
|
|
1242
|
-
// src/Foundation/RTC/
|
|
2031
|
+
// src/Foundation/RTC/TRTC/TrtcAudioVolumeSupport.ts
|
|
2032
|
+
function canControlTrtcRemoteAudioVolume(environment) {
|
|
2033
|
+
const userAgent = environment?.userAgent ?? (typeof navigator === "undefined" ? "" : navigator.userAgent);
|
|
2034
|
+
const maxTouchPoints = environment?.maxTouchPoints ?? (typeof navigator === "undefined" ? 0 : navigator.maxTouchPoints ?? 0);
|
|
2035
|
+
const ios = /iPhone|iPad|iPod/i.test(userAgent) || /Macintosh/i.test(userAgent) && maxTouchPoints > 1;
|
|
2036
|
+
return !ios;
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
// src/Foundation/RTC/TRTC/TrtcRtcManager.ts
|
|
1243
2040
|
var RTC_EVENT = {
|
|
1244
2041
|
remoteVideoAvailable: "remote-video-available",
|
|
1245
2042
|
remoteVideoUnavailable: "remote-video-unavailable",
|
|
@@ -1255,7 +2052,7 @@ var QOS_PREFERENCE_MAP = {
|
|
|
1255
2052
|
["maintainFramerate" /* maintainFramerate */]: "smooth",
|
|
1256
2053
|
["maintainQuality" /* maintainQuality */]: "clear"
|
|
1257
2054
|
};
|
|
1258
|
-
var
|
|
2055
|
+
var TrtcRtcManager = class {
|
|
1259
2056
|
/**
|
|
1260
2057
|
* 创建 RTC 管理器。
|
|
1261
2058
|
*
|
|
@@ -1343,13 +2140,17 @@ var RtcManager = class {
|
|
|
1343
2140
|
);
|
|
1344
2141
|
}
|
|
1345
2142
|
try {
|
|
2143
|
+
const captureSize = rtcOrientedVideoSize({
|
|
2144
|
+
width: options.width,
|
|
2145
|
+
height: options.height
|
|
2146
|
+
});
|
|
1346
2147
|
await engine.startLocalVideo({
|
|
1347
2148
|
publish: false,
|
|
1348
2149
|
option: {
|
|
1349
2150
|
useFrontCamera: options.position === "front" /* front */,
|
|
1350
2151
|
profile: {
|
|
1351
|
-
width:
|
|
1352
|
-
height:
|
|
2152
|
+
width: captureSize.width,
|
|
2153
|
+
height: captureSize.height,
|
|
1353
2154
|
frameRate: options.frameRate,
|
|
1354
2155
|
bitrate: CAPTURE_PLACEHOLDER_BITRATE
|
|
1355
2156
|
}
|
|
@@ -1419,6 +2220,9 @@ var RtcManager = class {
|
|
|
1419
2220
|
* @throws 引擎未初始化、进房参数无效或进房失败时抛出错误。
|
|
1420
2221
|
*/
|
|
1421
2222
|
async joinRoom(configuration) {
|
|
2223
|
+
if (configuration.provider !== "trtc") {
|
|
2224
|
+
throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "TRTC credentials required");
|
|
2225
|
+
}
|
|
1422
2226
|
const engine = this.requireEngine();
|
|
1423
2227
|
const sdkAppId = Number(configuration.sdkAppID);
|
|
1424
2228
|
if (!Number.isInteger(sdkAppId) || sdkAppId <= 0) {
|
|
@@ -1469,7 +2273,7 @@ var RtcManager = class {
|
|
|
1469
2273
|
/**
|
|
1470
2274
|
* 配置本地视频编码参数。
|
|
1471
2275
|
*
|
|
1472
|
-
* TRTC 只接受单一目标码率,以 `
|
|
2276
|
+
* TRTC 只接受单一目标码率,以 `minimumBitrate`(流畅优先档)作为目标码率;
|
|
1473
2277
|
* 编码策略偏好映射为 TRTC 弱网偏好。
|
|
1474
2278
|
*
|
|
1475
2279
|
* @throws 摄像头采集未启动或编码参数配置失败时抛出错误。
|
|
@@ -1483,13 +2287,17 @@ var RtcManager = class {
|
|
|
1483
2287
|
);
|
|
1484
2288
|
}
|
|
1485
2289
|
try {
|
|
2290
|
+
const encodingSize = rtcOrientedVideoSize({
|
|
2291
|
+
width: configuration.width,
|
|
2292
|
+
height: configuration.height
|
|
2293
|
+
});
|
|
1486
2294
|
await engine.updateLocalVideo({
|
|
1487
2295
|
option: {
|
|
1488
2296
|
profile: {
|
|
1489
|
-
width:
|
|
1490
|
-
height:
|
|
2297
|
+
width: encodingSize.width,
|
|
2298
|
+
height: encodingSize.height,
|
|
1491
2299
|
frameRate: configuration.frameRate,
|
|
1492
|
-
bitrate: configuration.
|
|
2300
|
+
bitrate: configuration.minimumBitrate
|
|
1493
2301
|
},
|
|
1494
2302
|
qosPreference: QOS_PREFERENCE_MAP[configuration.encoderPreference]
|
|
1495
2303
|
}
|
|
@@ -1607,14 +2415,34 @@ var RtcManager = class {
|
|
|
1607
2415
|
throw this.mapError(error);
|
|
1608
2416
|
}
|
|
1609
2417
|
}
|
|
2418
|
+
/**
|
|
2419
|
+
* 当前平台是否支持远端音量控制(iOS Safari/WKWebView 不支持)。
|
|
2420
|
+
*/
|
|
2421
|
+
get supportsRemoteAudioVolumeControl() {
|
|
2422
|
+
return canControlTrtcRemoteAudioVolume();
|
|
2423
|
+
}
|
|
1610
2424
|
/**
|
|
1611
2425
|
* 设置指定远端用户的音频播放音量。
|
|
1612
2426
|
*
|
|
2427
|
+
* iOS Safari/WKWebView 不允许 JS 修改媒体元素音量(TRTC 官方标注
|
|
2428
|
+
* `setRemoteAudioVolume` 不支持 iOS Safari),该平台上降级为
|
|
2429
|
+
* 播放/静音二值控制:音量为 0 时停止播放该路音频,非 0 时恢复播放;
|
|
2430
|
+
* 恢复播放需要重新拉流,会有短暂缓冲。
|
|
2431
|
+
*
|
|
1613
2432
|
* @param volume 音量,取值范围为 `0...100`,越界时收敛到边界。
|
|
1614
2433
|
*/
|
|
1615
2434
|
setRemoteAudioVolume(volume, userID) {
|
|
1616
2435
|
const engine = this.requireEngine();
|
|
1617
2436
|
const normalized = Math.min(Math.max(Math.round(volume), 0), 100);
|
|
2437
|
+
if (!canControlTrtcRemoteAudioVolume()) {
|
|
2438
|
+
void engine.muteRemoteAudio(userID, normalized === 0).catch((error) => {
|
|
2439
|
+
XmaxLogger.rtc.warning(
|
|
2440
|
+
() => `\u8FDC\u7AEF\u97F3\u9891\u64AD\u653E\u72B6\u6001\u5207\u6362\u5931\u8D25 (Failed to Toggle Remote Audio Playback)
|
|
2441
|
+
\u2514\u2500 ${XmaxLogger.localized("\u539F\u56E0\uFF1A", "Reason: ")}${XmaxError.from(error).message}`
|
|
2442
|
+
);
|
|
2443
|
+
});
|
|
2444
|
+
return;
|
|
2445
|
+
}
|
|
1618
2446
|
engine.setRemoteAudioVolume(userID, normalized);
|
|
1619
2447
|
}
|
|
1620
2448
|
/**
|
|
@@ -1622,7 +2450,7 @@ var RtcManager = class {
|
|
|
1622
2450
|
*
|
|
1623
2451
|
* @throws 未在房间中或消息编码后超过 1000 字节时抛出错误。
|
|
1624
2452
|
*/
|
|
1625
|
-
sendRoomMessage(message) {
|
|
2453
|
+
async sendRoomMessage(message) {
|
|
1626
2454
|
const engine = this.requireEngine();
|
|
1627
2455
|
if (!this.isInRoom) {
|
|
1628
2456
|
throw new XmaxError(
|
|
@@ -1642,6 +2470,11 @@ var RtcManager = class {
|
|
|
1642
2470
|
data: encoded.buffer
|
|
1643
2471
|
});
|
|
1644
2472
|
}
|
|
2473
|
+
/**
|
|
2474
|
+
* TRTC 凭据由会话层缓存,保持现有进房鉴权行为。
|
|
2475
|
+
*/
|
|
2476
|
+
async updateCredentials(_configuration) {
|
|
2477
|
+
}
|
|
1645
2478
|
/**
|
|
1646
2479
|
* 设置 RTC 事件监听器,传入空值时清除监听器。
|
|
1647
2480
|
*/
|
|
@@ -1736,104 +2569,681 @@ var RtcManager = class {
|
|
|
1736
2569
|
* 返回当前引擎实例;未初始化时抛出错误。
|
|
1737
2570
|
*/
|
|
1738
2571
|
requireEngine() {
|
|
1739
|
-
const engine = this.lease?.engine;
|
|
1740
|
-
if (!engine) {
|
|
1741
|
-
throw new XmaxError(
|
|
1742
|
-
"RTC_ERROR" /* rtcError */,
|
|
1743
|
-
"RTC engine is not initialized"
|
|
1744
|
-
);
|
|
1745
|
-
}
|
|
1746
|
-
return engine;
|
|
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
|
+
});
|
|
3081
|
+
}
|
|
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();
|
|
3100
|
+
});
|
|
3101
|
+
}
|
|
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
|
+
}
|
|
3188
|
+
requireEngine() {
|
|
3189
|
+
if (!this.engine) throw new XmaxError("RTC_ERROR" /* rtcError */, "VeRTC is not initialized");
|
|
3190
|
+
return this.engine;
|
|
1747
3191
|
}
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
const message = typeof rtcError?.message === "string" && rtcError.message.trim().length > 0 ? rtcError.message.trim() : String(error);
|
|
1758
|
-
if (originName === "NotAllowedError" || originName === "SecurityError" || /NotAllowed/i.test(message)) {
|
|
1759
|
-
return new XmaxError("CAMERA_PERMISSION_DENIED" /* cameraPermissionDenied */, message);
|
|
1760
|
-
}
|
|
1761
|
-
if (originName) {
|
|
1762
|
-
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);
|
|
1763
3201
|
}
|
|
1764
|
-
|
|
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");
|
|
1765
3212
|
}
|
|
1766
3213
|
};
|
|
1767
3214
|
|
|
1768
|
-
// src/Foundation/
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
if (!size || !Number.isInteger(size)) return false;
|
|
1778
|
-
const histogram = new Uint32Array(256);
|
|
1779
|
-
let total = 0;
|
|
1780
|
-
let dark = 0;
|
|
1781
|
-
for (let i = 0; i < pixels.length; i += 4) {
|
|
1782
|
-
const luminance = Math.round(0.2126 * pixels[i] + 0.7152 * pixels[i + 1] + 0.0722 * pixels[i + 2]);
|
|
1783
|
-
histogram[luminance] = histogram[luminance] + 1;
|
|
1784
|
-
total += luminance;
|
|
1785
|
-
if (luminance < 12) dark++;
|
|
1786
|
-
}
|
|
1787
|
-
let median = 0;
|
|
1788
|
-
let accumulated = 0;
|
|
1789
|
-
for (; median < 255; median++) {
|
|
1790
|
-
accumulated += histogram[median];
|
|
1791
|
-
if (accumulated >= size / 2) break;
|
|
1792
|
-
}
|
|
1793
|
-
const mean = total / size;
|
|
1794
|
-
return mean >= 24 && median >= 18 && dark / size <= 0.75;
|
|
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();
|
|
1795
3224
|
}
|
|
1796
|
-
}
|
|
3225
|
+
}
|
|
3226
|
+
|
|
3227
|
+
// src/Foundation/Media/Camera/CameraFrameValidator.ts
|
|
3228
|
+
var CAMERA_WARMUP_MS = 200;
|
|
1797
3229
|
function waitForValidCameraFrame(track, signal) {
|
|
1798
3230
|
return new Promise((resolve, reject) => {
|
|
1799
|
-
let
|
|
1800
|
-
let
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
const gate = new CameraFrameValidator();
|
|
1806
|
-
const finish = (result = "valid") => {
|
|
1807
|
-
if (finished) return;
|
|
1808
|
-
finished = true;
|
|
1809
|
-
clearTimeout(timeout);
|
|
1810
|
-
clearTimeout(poll);
|
|
1811
|
-
if (video) {
|
|
1812
|
-
if (frameCallback !== void 0) video.cancelVideoFrameCallback(frameCallback);
|
|
1813
|
-
video.pause();
|
|
1814
|
-
video.srcObject = null;
|
|
1815
|
-
video.remove();
|
|
1816
|
-
}
|
|
3231
|
+
let timer;
|
|
3232
|
+
let settled = false;
|
|
3233
|
+
const finish = (error) => {
|
|
3234
|
+
if (settled) return;
|
|
3235
|
+
settled = true;
|
|
3236
|
+
clearTimeout(timer);
|
|
1817
3237
|
signal.removeEventListener("abort", abort);
|
|
1818
3238
|
track.removeEventListener("ended", ended);
|
|
1819
|
-
if (
|
|
1820
|
-
|
|
1821
|
-
reject(result);
|
|
3239
|
+
if (error) {
|
|
3240
|
+
reject(error);
|
|
1822
3241
|
} else {
|
|
1823
|
-
if (result === "timeout") {
|
|
1824
|
-
XmaxLogger.media.warning(() => XmaxLogger.localized(
|
|
1825
|
-
"\u76F8\u673A\u4EAE\u5EA6\u68C0\u67E5\u8D85\u65F6\uFF0C\u6309\u5F53\u524D\u753B\u9762\u7EE7\u7EED\u751F\u6210 (Camera Frame Validation Timed Out; Continuing)\n\u2514\u2500 \u672A\u83B7\u5F97\u4EAE\u5EA6\u5408\u683C\u5E27\uFF0C\u4E0D\u518D\u7B49\u5F85\u3002",
|
|
1826
|
-
"Camera Frame Validation Timed Out; Continuing\n\u2514\u2500 No sufficiently bright frame received; proceeding without further waiting."
|
|
1827
|
-
));
|
|
1828
|
-
} else {
|
|
1829
|
-
XmaxLogger.media.info(() => `\u76F8\u673A\u66DD\u5149\u68C0\u67E5\u901A\u8FC7 (Camera Exposure Ready)
|
|
1830
|
-
\u2514\u2500 ${Math.round(performance.now() - started)} ms`);
|
|
1831
|
-
}
|
|
1832
3242
|
resolve();
|
|
1833
3243
|
}
|
|
1834
3244
|
};
|
|
1835
|
-
const abort = () => finish(new XmaxError("CANCELLED" /* cancelled */, "Camera
|
|
1836
|
-
const ended = () => finish(new XmaxError("MEDIA_ERROR" /* mediaError */, "Camera track ended before
|
|
3245
|
+
const abort = () => finish(new XmaxError("CANCELLED" /* cancelled */, "Camera warmup cancelled"));
|
|
3246
|
+
const ended = () => finish(new XmaxError("MEDIA_ERROR" /* mediaError */, "Camera track ended before warmup completed"));
|
|
1837
3247
|
signal.addEventListener("abort", abort, { once: true });
|
|
1838
3248
|
track.addEventListener("ended", ended, { once: true });
|
|
1839
3249
|
if (signal.aborted) {
|
|
@@ -1844,70 +3254,7 @@ function waitForValidCameraFrame(track, signal) {
|
|
|
1844
3254
|
ended();
|
|
1845
3255
|
return;
|
|
1846
3256
|
}
|
|
1847
|
-
|
|
1848
|
-
video = document.createElement("video");
|
|
1849
|
-
video.muted = true;
|
|
1850
|
-
video.autoplay = true;
|
|
1851
|
-
video.playsInline = true;
|
|
1852
|
-
video.setAttribute("aria-hidden", "true");
|
|
1853
|
-
video.style.cssText = "position:fixed;left:0;top:0;width:1px;height:1px;opacity:0;pointer-events:none;";
|
|
1854
|
-
const canvas = document.createElement("canvas");
|
|
1855
|
-
canvas.width = 64;
|
|
1856
|
-
canvas.height = 48;
|
|
1857
|
-
const context = canvas.getContext("2d", { willReadFrequently: true });
|
|
1858
|
-
if (!context) throw new Error("Camera exposure sampling requires a 2D canvas");
|
|
1859
|
-
video.srcObject = new MediaStream([track]);
|
|
1860
|
-
document.body.appendChild(video);
|
|
1861
|
-
const source = video;
|
|
1862
|
-
let lastFrameTime = -Infinity;
|
|
1863
|
-
const sample = (mediaTime) => {
|
|
1864
|
-
if (source.readyState < 2 || !source.videoWidth || !source.videoHeight || track.muted || !track.enabled) {
|
|
1865
|
-
return;
|
|
1866
|
-
}
|
|
1867
|
-
if (mediaTime === lastFrameTime) return;
|
|
1868
|
-
lastFrameTime = mediaTime;
|
|
1869
|
-
const width = source.videoWidth;
|
|
1870
|
-
const height = source.videoHeight;
|
|
1871
|
-
context.drawImage(source, width * 0.2, height * 0.2, width * 0.6, height * 0.6, 0, 0, 64, 48);
|
|
1872
|
-
if (gate.accept(context.getImageData(0, 0, 64, 48).data)) finish();
|
|
1873
|
-
};
|
|
1874
|
-
const fail = (error) => finish(new XmaxError(
|
|
1875
|
-
"MEDIA_ERROR" /* mediaError */,
|
|
1876
|
-
`Camera exposure check failed: ${error instanceof Error ? error.message : String(error)}`
|
|
1877
|
-
));
|
|
1878
|
-
const schedule = () => {
|
|
1879
|
-
if (finished) return;
|
|
1880
|
-
if (typeof source.requestVideoFrameCallback === "function") {
|
|
1881
|
-
frameCallback = source.requestVideoFrameCallback((_now, metadata) => {
|
|
1882
|
-
frameCallback = void 0;
|
|
1883
|
-
try {
|
|
1884
|
-
sample(metadata.mediaTime);
|
|
1885
|
-
schedule();
|
|
1886
|
-
} catch (error) {
|
|
1887
|
-
fail(error);
|
|
1888
|
-
}
|
|
1889
|
-
});
|
|
1890
|
-
} else {
|
|
1891
|
-
poll = setTimeout(() => {
|
|
1892
|
-
try {
|
|
1893
|
-
const frameTime = typeof source.getVideoPlaybackQuality === "function" ? source.getVideoPlaybackQuality().totalVideoFrames : source.currentTime;
|
|
1894
|
-
sample(frameTime);
|
|
1895
|
-
schedule();
|
|
1896
|
-
} catch (error) {
|
|
1897
|
-
fail(error);
|
|
1898
|
-
}
|
|
1899
|
-
}, FALLBACK_POLL_INTERVAL_MS);
|
|
1900
|
-
}
|
|
1901
|
-
};
|
|
1902
|
-
timeout = setTimeout(() => finish("timeout"), TIMEOUT_MS);
|
|
1903
|
-
XmaxLogger.media.info(() => "\u7B49\u5F85\u9996\u5F20\u4EAE\u5EA6\u5408\u683C\u5E27 (Waiting for the First Sufficiently Bright Frame)");
|
|
1904
|
-
void source.play().then(schedule, fail);
|
|
1905
|
-
} catch (error) {
|
|
1906
|
-
finish(new XmaxError(
|
|
1907
|
-
"MEDIA_ERROR" /* mediaError */,
|
|
1908
|
-
`Camera exposure check failed: ${error instanceof Error ? error.message : String(error)}`
|
|
1909
|
-
));
|
|
1910
|
-
}
|
|
3257
|
+
timer = setTimeout(() => finish(), CAMERA_WARMUP_MS);
|
|
1911
3258
|
});
|
|
1912
3259
|
}
|
|
1913
3260
|
|
|
@@ -2126,7 +3473,7 @@ var _CameraController = class _CameraController {
|
|
|
2126
3473
|
__publicField(this, "storedUseMicrophone", false);
|
|
2127
3474
|
__publicField(this, "storedFrameValidationEnabled", true);
|
|
2128
3475
|
this.permissionManager = options?.permissionManager ?? new PermissionManager();
|
|
2129
|
-
this.rtcManager = options?.rtcManager ?? new
|
|
3476
|
+
this.rtcManager = options?.rtcManager ?? new TrtcRtcManager();
|
|
2130
3477
|
this.mediaService = options?.mediaService ?? new MediaService();
|
|
2131
3478
|
this.errorListener = options?.errorListener ?? (() => {
|
|
2132
3479
|
});
|
|
@@ -2144,13 +3491,13 @@ var _CameraController = class _CameraController {
|
|
|
2144
3491
|
return this.activeTrack !== void 0 && this.storedUseMicrophone;
|
|
2145
3492
|
}
|
|
2146
3493
|
/**
|
|
2147
|
-
*
|
|
3494
|
+
* 当前活动相机流是否启用发布前预热等待;没有活动流时为 false。
|
|
2148
3495
|
*/
|
|
2149
3496
|
get isFrameValidationEnabled() {
|
|
2150
3497
|
return this.activeTrack !== void 0 && this.storedFrameValidationEnabled;
|
|
2151
3498
|
}
|
|
2152
3499
|
/**
|
|
2153
|
-
*
|
|
3500
|
+
* 等待当前轨道预热完成(固定 200ms);禁用时跳过,取消或轨道被替换时拒绝。
|
|
2154
3501
|
*/
|
|
2155
3502
|
async waitForValidCameraFrame(signal) {
|
|
2156
3503
|
const track = this.activeTrack;
|
|
@@ -2160,7 +3507,7 @@ var _CameraController = class _CameraController {
|
|
|
2160
3507
|
if (!this.isFrameValidationEnabled) return;
|
|
2161
3508
|
await waitForValidCameraFrame(mediaTrack, signal);
|
|
2162
3509
|
if (this.activeTrack !== track || track.mediaStreamTrack !== mediaTrack) {
|
|
2163
|
-
throw new XmaxError("CANCELLED" /* cancelled */, "Camera track changed during
|
|
3510
|
+
throw new XmaxError("CANCELLED" /* cancelled */, "Camera track changed during warmup");
|
|
2164
3511
|
}
|
|
2165
3512
|
}
|
|
2166
3513
|
/**
|
|
@@ -2179,7 +3526,7 @@ var _CameraController = class _CameraController {
|
|
|
2179
3526
|
* @param options.videoFormat 期望的输出尺寸、帧率和编码配置;尺寸按模型规则调整。
|
|
2180
3527
|
* @param options.position 首次启动时使用的摄像头位置。
|
|
2181
3528
|
* @param options.useMicrophone 是否申请麦克风权限并允许实时连接时启动音频采集。
|
|
2182
|
-
* @param options.enableFrameValidation
|
|
3529
|
+
* @param options.enableFrameValidation 是否在发布前等待相机预热(固定 200ms),默认 true;false 跳过等待。
|
|
2183
3530
|
* @returns 包含本地相机视频轨道的媒体流。
|
|
2184
3531
|
* @throws 已有活动相机流、格式无效、权限不足或采集启动失败时抛出错误。
|
|
2185
3532
|
*/
|
|
@@ -2417,6 +3764,53 @@ var RealtimeSession = class {
|
|
|
2417
3764
|
}
|
|
2418
3765
|
};
|
|
2419
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
|
+
|
|
2420
3814
|
// src/Service/Realtime/RealtimeSessionConnection.ts
|
|
2421
3815
|
var RealtimeSessionConnection = class {
|
|
2422
3816
|
/**
|
|
@@ -2435,7 +3829,7 @@ var RealtimeSessionConnection = class {
|
|
|
2435
3829
|
* RTC 提供方
|
|
2436
3830
|
*/
|
|
2437
3831
|
/**
|
|
2438
|
-
*
|
|
3832
|
+
* TRTC 凭据的提供方判别字段。
|
|
2439
3833
|
*/
|
|
2440
3834
|
__publicField(this, "provider");
|
|
2441
3835
|
/**
|
|
@@ -2477,6 +3871,12 @@ var RealtimeSessionConnection = class {
|
|
|
2477
3871
|
this.botID = init.botID;
|
|
2478
3872
|
}
|
|
2479
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
|
+
}
|
|
2480
3880
|
|
|
2481
3881
|
// src/Service/Realtime/RealtimeSessionService.ts
|
|
2482
3882
|
var RealtimeSessionService = class _RealtimeSessionService {
|
|
@@ -2492,6 +3892,7 @@ var RealtimeSessionService = class _RealtimeSessionService {
|
|
|
2492
3892
|
* 服务层组件
|
|
2493
3893
|
*/
|
|
2494
3894
|
__publicField(this, "apiService");
|
|
3895
|
+
__publicField(this, "provider");
|
|
2495
3896
|
/**
|
|
2496
3897
|
* 心跳配置
|
|
2497
3898
|
*/
|
|
@@ -2501,7 +3902,16 @@ var RealtimeSessionService = class _RealtimeSessionService {
|
|
|
2501
3902
|
* 运行状态
|
|
2502
3903
|
*/
|
|
2503
3904
|
__publicField(this, "heartbeatVersion", 0);
|
|
3905
|
+
/**
|
|
3906
|
+
* 会话凭证缓存,支持心跳仅返回变更字段;关闭后清除。
|
|
3907
|
+
*/
|
|
3908
|
+
__publicField(this, "credentials", /* @__PURE__ */ new Map());
|
|
3909
|
+
__publicField(this, "refreshes", /* @__PURE__ */ new Map());
|
|
2504
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
|
+
}
|
|
2505
3915
|
this.heartbeatIntervalMs = options.heartbeatIntervalMs ?? 1e4;
|
|
2506
3916
|
this.sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
2507
3917
|
}
|
|
@@ -2512,7 +3922,18 @@ var RealtimeSessionService = class _RealtimeSessionService {
|
|
|
2512
3922
|
const payload = await this.apiService.post("/session", {
|
|
2513
3923
|
model
|
|
2514
3924
|
});
|
|
2515
|
-
|
|
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
|
+
}
|
|
2516
3937
|
}
|
|
2517
3938
|
/**
|
|
2518
3939
|
* 启动指定会话的周期心跳;重复启动会替换当前心跳。
|
|
@@ -2528,12 +3949,15 @@ var RealtimeSessionService = class _RealtimeSessionService {
|
|
|
2528
3949
|
this.heartbeatVersion += 1;
|
|
2529
3950
|
}
|
|
2530
3951
|
/**
|
|
2531
|
-
*
|
|
3952
|
+
* 关闭指定实时会话;请求带 keepalive,页面卸载后仍能发出。
|
|
2532
3953
|
*/
|
|
2533
3954
|
async closeSession(sessionID) {
|
|
3955
|
+
this.credentials.delete(sessionID);
|
|
3956
|
+
this.refreshes.delete(sessionID);
|
|
2534
3957
|
try {
|
|
2535
3958
|
await this.apiService.delete(
|
|
2536
|
-
`/session/${sessionID}
|
|
3959
|
+
`/session/${sessionID}`,
|
|
3960
|
+
{ keepalive: true }
|
|
2537
3961
|
);
|
|
2538
3962
|
} catch (error) {
|
|
2539
3963
|
throw XmaxError.from(error);
|
|
@@ -2554,7 +3978,7 @@ var RealtimeSessionService = class _RealtimeSessionService {
|
|
|
2554
3978
|
return;
|
|
2555
3979
|
}
|
|
2556
3980
|
_RealtimeSessionService.ensureSessionActive(session);
|
|
2557
|
-
handlers.onRefresh?.(session);
|
|
3981
|
+
await handlers.onRefresh?.(session);
|
|
2558
3982
|
} catch (error) {
|
|
2559
3983
|
if (this.heartbeatVersion !== version) {
|
|
2560
3984
|
return;
|
|
@@ -2568,11 +3992,35 @@ var RealtimeSessionService = class _RealtimeSessionService {
|
|
|
2568
3992
|
/**
|
|
2569
3993
|
* 发送一次心跳请求并解析会话数据(不要求携带完整连接参数)。
|
|
2570
3994
|
*/
|
|
2571
|
-
|
|
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);
|
|
2572
4011
|
const payload = await this.apiService.put(
|
|
2573
4012
|
`/session/${sessionID}/heartbeat`
|
|
2574
4013
|
);
|
|
2575
|
-
|
|
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;
|
|
2576
4024
|
}
|
|
2577
4025
|
/**
|
|
2578
4026
|
* 校验会话仍处于活跃状态;状态明确非 ACTIVE 时抛出会话错误。
|
|
@@ -2589,12 +4037,12 @@ var RealtimeSessionService = class _RealtimeSessionService {
|
|
|
2589
4037
|
/**
|
|
2590
4038
|
* 由会话接口数据构造会话模型;`requiresConnection` 要求携带完整 RTC 连接参数。
|
|
2591
4039
|
*/
|
|
2592
|
-
|
|
4040
|
+
makeSession(payload, requiresConnection) {
|
|
2593
4041
|
const sessionID = _RealtimeSessionService.nonEmpty(payload.sessionUid);
|
|
2594
4042
|
if (!sessionID) {
|
|
2595
4043
|
throw new XmaxError("SESSION_ERROR" /* sessionError */, "Invalid session response");
|
|
2596
4044
|
}
|
|
2597
|
-
const connection =
|
|
4045
|
+
const connection = this.makeConnection(payload.modelExtra, payload.userUid);
|
|
2598
4046
|
if (requiresConnection && !connection) {
|
|
2599
4047
|
throw new XmaxError(
|
|
2600
4048
|
"SESSION_ERROR" /* sessionError */,
|
|
@@ -2610,19 +4058,30 @@ var RealtimeSessionService = class _RealtimeSessionService {
|
|
|
2610
4058
|
});
|
|
2611
4059
|
}
|
|
2612
4060
|
/**
|
|
2613
|
-
* 解析 `modelExtra` 中的
|
|
4061
|
+
* 解析 `modelExtra` 中的 RTC 连接参数;字段不完整或提供方不支持时返回空。
|
|
2614
4062
|
*/
|
|
2615
|
-
|
|
4063
|
+
makeConnection(modelExtra, fallbackUserID) {
|
|
2616
4064
|
const payload = _RealtimeSessionService.decodeConnectionPayload(modelExtra);
|
|
2617
4065
|
if (!payload) {
|
|
2618
4066
|
return void 0;
|
|
2619
4067
|
}
|
|
2620
|
-
|
|
2621
|
-
return void 0;
|
|
2622
|
-
}
|
|
4068
|
+
const provider = this.provider;
|
|
2623
4069
|
const roomID = _RealtimeSessionService.nonEmpty(payload.room_id);
|
|
2624
4070
|
const sdkAppID = _RealtimeSessionService.nonEmpty(payload.rtc_app_id);
|
|
2625
|
-
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
|
+
}
|
|
2626
4085
|
const userSig = _RealtimeSessionService.nonEmpty(payload.user_sig);
|
|
2627
4086
|
const privateMapKey = _RealtimeSessionService.nonEmpty(
|
|
2628
4087
|
payload.private_map_key_with_string_room_id
|
|
@@ -2646,12 +4105,12 @@ var RealtimeSessionService = class _RealtimeSessionService {
|
|
|
2646
4105
|
static decodeConnectionPayload(modelExtra) {
|
|
2647
4106
|
if (typeof modelExtra === "string") {
|
|
2648
4107
|
try {
|
|
2649
|
-
return JSON.parse(modelExtra);
|
|
4108
|
+
return _RealtimeSessionService.decodeConnectionPayload(JSON.parse(modelExtra));
|
|
2650
4109
|
} catch {
|
|
2651
4110
|
return void 0;
|
|
2652
4111
|
}
|
|
2653
4112
|
}
|
|
2654
|
-
if (typeof modelExtra === "object" && modelExtra !== null) {
|
|
4113
|
+
if (typeof modelExtra === "object" && modelExtra !== null && !Array.isArray(modelExtra)) {
|
|
2655
4114
|
return modelExtra;
|
|
2656
4115
|
}
|
|
2657
4116
|
return void 0;
|
|
@@ -2660,7 +4119,7 @@ var RealtimeSessionService = class _RealtimeSessionService {
|
|
|
2660
4119
|
* 归一化可选字符串:去除首尾空白后为空时返回 `undefined`。
|
|
2661
4120
|
*/
|
|
2662
4121
|
static nonEmpty(value) {
|
|
2663
|
-
const normalized = value
|
|
4122
|
+
const normalized = typeof value === "string" ? value.trim() : void 0;
|
|
2664
4123
|
return normalized || void 0;
|
|
2665
4124
|
}
|
|
2666
4125
|
};
|
|
@@ -2767,49 +4226,6 @@ var RemoteStream = class {
|
|
|
2767
4226
|
}
|
|
2768
4227
|
};
|
|
2769
4228
|
|
|
2770
|
-
// src/Foundation/RTC/RoomJoinConfiguration.ts
|
|
2771
|
-
var RoomJoinConfiguration = class {
|
|
2772
|
-
/**
|
|
2773
|
-
* 创建房间加入参数。
|
|
2774
|
-
*
|
|
2775
|
-
* @param init.roomID TRTC 房间号。
|
|
2776
|
-
* @param init.userID TRTC 登录身份。
|
|
2777
|
-
* @param init.sdkAppID TRTC SDKAppID(字符串形式)。
|
|
2778
|
-
* @param init.userSig TRTC 登录签名。
|
|
2779
|
-
* @param init.privateMapKey TRTC 进房鉴权字段。
|
|
2780
|
-
*/
|
|
2781
|
-
constructor(init) {
|
|
2782
|
-
/**
|
|
2783
|
-
* 进房参数
|
|
2784
|
-
*/
|
|
2785
|
-
/**
|
|
2786
|
-
* TRTC 房间号(字符串房间号)。
|
|
2787
|
-
*/
|
|
2788
|
-
__publicField(this, "roomID");
|
|
2789
|
-
/**
|
|
2790
|
-
* TRTC 登录身份(`rtc_user_id`)。
|
|
2791
|
-
*/
|
|
2792
|
-
__publicField(this, "userID");
|
|
2793
|
-
/**
|
|
2794
|
-
* TRTC SDKAppID(字符串形式,进房时转换为数值)。
|
|
2795
|
-
*/
|
|
2796
|
-
__publicField(this, "sdkAppID");
|
|
2797
|
-
/**
|
|
2798
|
-
* TRTC 登录签名。
|
|
2799
|
-
*/
|
|
2800
|
-
__publicField(this, "userSig");
|
|
2801
|
-
/**
|
|
2802
|
-
* TRTC 进房鉴权字段(字符串房间号对应的 privateMapKey)。
|
|
2803
|
-
*/
|
|
2804
|
-
__publicField(this, "privateMapKey");
|
|
2805
|
-
this.roomID = init.roomID;
|
|
2806
|
-
this.userID = init.userID;
|
|
2807
|
-
this.sdkAppID = init.sdkAppID;
|
|
2808
|
-
this.userSig = init.userSig;
|
|
2809
|
-
this.privateMapKey = init.privateMapKey;
|
|
2810
|
-
}
|
|
2811
|
-
};
|
|
2812
|
-
|
|
2813
4229
|
// src/Stream/Room/RoomEvent.ts
|
|
2814
4230
|
var RoomEvent = class _RoomEvent {
|
|
2815
4231
|
/**
|
|
@@ -2960,7 +4376,7 @@ var RoomHeartbeat = class {
|
|
|
2960
4376
|
return;
|
|
2961
4377
|
}
|
|
2962
4378
|
try {
|
|
2963
|
-
this.rtcManager.sendRoomMessage(RoomEvent.heartbeat({ userID }));
|
|
4379
|
+
await this.rtcManager.sendRoomMessage(RoomEvent.heartbeat({ userID }));
|
|
2964
4380
|
} catch (error) {
|
|
2965
4381
|
if (this.cycleVersion !== version) {
|
|
2966
4382
|
return;
|
|
@@ -2977,7 +4393,11 @@ var RoomHeartbeat = class {
|
|
|
2977
4393
|
// src/Stream/Room/RoomMessageCodec.ts
|
|
2978
4394
|
var CHUNK_EVENT = "__trtc_chunk__";
|
|
2979
4395
|
var _RoomMessageCodec = class _RoomMessageCodec {
|
|
2980
|
-
|
|
4396
|
+
/**
|
|
4397
|
+
* 创建厂商协议编解码器;TRTC 保持原事件名,Agora 使用 __agora_chunk__;null 表示不拆包。
|
|
4398
|
+
*/
|
|
4399
|
+
constructor(chunkEvent = CHUNK_EVENT) {
|
|
4400
|
+
__publicField(this, "chunkEvent", chunkEvent);
|
|
2981
4401
|
/**
|
|
2982
4402
|
* 接收缓存
|
|
2983
4403
|
*/
|
|
@@ -2987,10 +4407,10 @@ var _RoomMessageCodec = class _RoomMessageCodec {
|
|
|
2987
4407
|
* 编码出站消息。
|
|
2988
4408
|
*
|
|
2989
4409
|
* @returns 不超过阈值时返回仅含原消息的单元素数组;
|
|
2990
|
-
*
|
|
4410
|
+
* 否则返回按当前提供方分片协议包装的 JSON 数组。
|
|
2991
4411
|
*/
|
|
2992
4412
|
encodeOutgoing(message) {
|
|
2993
|
-
if (_RoomMessageCodec.byteLength(message) <= _RoomMessageCodec.chunkThresholdBytes) {
|
|
4413
|
+
if (this.chunkEvent === null || _RoomMessageCodec.byteLength(message) <= _RoomMessageCodec.chunkThresholdBytes) {
|
|
2994
4414
|
return [message];
|
|
2995
4415
|
}
|
|
2996
4416
|
const eventId = _RoomMessageCodec.makeEventId();
|
|
@@ -3005,7 +4425,7 @@ var _RoomMessageCodec = class _RoomMessageCodec {
|
|
|
3005
4425
|
);
|
|
3006
4426
|
return slices.map(
|
|
3007
4427
|
(data, index) => JSON.stringify({
|
|
3008
|
-
event:
|
|
4428
|
+
event: this.chunkEvent,
|
|
3009
4429
|
eventId,
|
|
3010
4430
|
index,
|
|
3011
4431
|
count,
|
|
@@ -3027,7 +4447,7 @@ var _RoomMessageCodec = class _RoomMessageCodec {
|
|
|
3027
4447
|
XmaxLogger.room.warning(() => "\u623F\u95F4\u6D88\u606F\u4E0D\u662F\u5408\u6CD5 JSON (Room Message Is Not Valid JSON)");
|
|
3028
4448
|
return void 0;
|
|
3029
4449
|
}
|
|
3030
|
-
const chunk =
|
|
4450
|
+
const chunk = this.parseChunk(parsed);
|
|
3031
4451
|
if (!chunk) {
|
|
3032
4452
|
return parsed;
|
|
3033
4453
|
}
|
|
@@ -3071,12 +4491,12 @@ var _RoomMessageCodec = class _RoomMessageCodec {
|
|
|
3071
4491
|
/**
|
|
3072
4492
|
* 从已解析的值识别分片消息;字段不完整时按普通业务消息处理。
|
|
3073
4493
|
*/
|
|
3074
|
-
|
|
3075
|
-
if (typeof parsed !== "object" || parsed === null) {
|
|
4494
|
+
parseChunk(parsed) {
|
|
4495
|
+
if (this.chunkEvent === null || typeof parsed !== "object" || parsed === null) {
|
|
3076
4496
|
return void 0;
|
|
3077
4497
|
}
|
|
3078
4498
|
const record = parsed;
|
|
3079
|
-
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") {
|
|
3080
4500
|
return void 0;
|
|
3081
4501
|
}
|
|
3082
4502
|
const index = record.index;
|
|
@@ -3106,7 +4526,7 @@ var _RoomMessageCodec = class _RoomMessageCodec {
|
|
|
3106
4526
|
let current = "";
|
|
3107
4527
|
let currentBytes = 0;
|
|
3108
4528
|
for (const char of message) {
|
|
3109
|
-
const bytes = _RoomMessageCodec.byteLength(char);
|
|
4529
|
+
const bytes = _RoomMessageCodec.byteLength(JSON.stringify(char)) - 2;
|
|
3110
4530
|
if (currentBytes + bytes > budget && current.length > 0) {
|
|
3111
4531
|
slices.push(current);
|
|
3112
4532
|
current = "";
|
|
@@ -3175,6 +4595,8 @@ var RoomController = class {
|
|
|
3175
4595
|
*/
|
|
3176
4596
|
__publicField(this, "heartbeat");
|
|
3177
4597
|
__publicField(this, "codec");
|
|
4598
|
+
__publicField(this, "customCodec");
|
|
4599
|
+
__publicField(this, "sendTail", Promise.resolve());
|
|
3178
4600
|
/**
|
|
3179
4601
|
* 事件监听
|
|
3180
4602
|
*/
|
|
@@ -3187,6 +4609,7 @@ var RoomController = class {
|
|
|
3187
4609
|
this.rtcManager = options.rtcManager;
|
|
3188
4610
|
this.heartbeat = options.heartbeat ?? new RoomHeartbeat({ rtcManager: options.rtcManager });
|
|
3189
4611
|
this.codec = options.codec ?? new RoomMessageCodec();
|
|
4612
|
+
this.customCodec = options.codec !== void 0;
|
|
3190
4613
|
}
|
|
3191
4614
|
/**
|
|
3192
4615
|
* 加入实时房间,并在异步边界前后确认连接操作仍然有效。
|
|
@@ -3209,15 +4632,10 @@ var RoomController = class {
|
|
|
3209
4632
|
this.state = { kind: "joining", operationID };
|
|
3210
4633
|
this.heartbeat.stop();
|
|
3211
4634
|
try {
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
roomID: connection.roomID,
|
|
3215
|
-
userID: connection.userID,
|
|
3216
|
-
sdkAppID: connection.sdkAppID,
|
|
3217
|
-
userSig: connection.userSig,
|
|
3218
|
-
privateMapKey: connection.privateMapKey
|
|
3219
|
-
})
|
|
4635
|
+
if (!this.customCodec) this.codec = new RoomMessageCodec(
|
|
4636
|
+
connection.provider === "vertc" ? null : connection.provider === "agora" ? "__agora_chunk__" : "__trtc_chunk__"
|
|
3220
4637
|
);
|
|
4638
|
+
await this.rtcManager.joinRoom(toRoomJoinConfiguration(connection));
|
|
3221
4639
|
ensureActive();
|
|
3222
4640
|
if (this.state.kind !== "joining" || this.state.operationID !== operationID || this.leaveOperation) {
|
|
3223
4641
|
throw new XmaxError("CANCELLED" /* cancelled */, "RTC room join was cancelled");
|
|
@@ -3257,7 +4675,7 @@ var RoomController = class {
|
|
|
3257
4675
|
* @throws 房间未就绪或信令发送失败时抛出错误。
|
|
3258
4676
|
*/
|
|
3259
4677
|
startGeneration(options) {
|
|
3260
|
-
this.send(
|
|
4678
|
+
return this.send(
|
|
3261
4679
|
RoomEvent.start({
|
|
3262
4680
|
userID: this.requireUserID(),
|
|
3263
4681
|
taskID: options.taskID,
|
|
@@ -3273,7 +4691,7 @@ var RoomController = class {
|
|
|
3273
4691
|
* @throws 房间未就绪或信令发送失败时抛出错误。
|
|
3274
4692
|
*/
|
|
3275
4693
|
changeGenerationCondition(options) {
|
|
3276
|
-
this.send(
|
|
4694
|
+
return this.send(
|
|
3277
4695
|
RoomEvent.changeCondition({
|
|
3278
4696
|
userID: this.requireUserID(),
|
|
3279
4697
|
taskID: options.taskID,
|
|
@@ -3290,7 +4708,7 @@ var RoomController = class {
|
|
|
3290
4708
|
*/
|
|
3291
4709
|
changeTargetSize(options) {
|
|
3292
4710
|
options.ensureActive();
|
|
3293
|
-
this.send(
|
|
4711
|
+
return this.send(
|
|
3294
4712
|
RoomEvent.changeTargetSize({
|
|
3295
4713
|
userID: this.requireUserID(),
|
|
3296
4714
|
taskID: options.taskID,
|
|
@@ -3303,18 +4721,18 @@ var RoomController = class {
|
|
|
3303
4721
|
*/
|
|
3304
4722
|
stopGeneration(taskID) {
|
|
3305
4723
|
if (!taskID || this.state.kind !== "joined") {
|
|
3306
|
-
return;
|
|
4724
|
+
return Promise.resolve();
|
|
3307
4725
|
}
|
|
3308
|
-
this.send(RoomEvent.stop({ userID: this.state.userID, taskID }));
|
|
4726
|
+
return this.send(RoomEvent.stop({ userID: this.state.userID, taskID }));
|
|
3309
4727
|
}
|
|
3310
4728
|
/**
|
|
3311
4729
|
* 发送生成任务的交互轨迹;任务标识或轨迹为空时忽略。
|
|
3312
4730
|
*/
|
|
3313
4731
|
sendTracks(taskID, points) {
|
|
3314
4732
|
if (!taskID || points.length === 0) {
|
|
3315
|
-
return;
|
|
4733
|
+
return Promise.resolve();
|
|
3316
4734
|
}
|
|
3317
|
-
this.send(
|
|
4735
|
+
return this.send(
|
|
3318
4736
|
RoomEvent.tracks({
|
|
3319
4737
|
userID: this.requireUserID(),
|
|
3320
4738
|
taskID,
|
|
@@ -3335,8 +4753,12 @@ var RoomController = class {
|
|
|
3335
4753
|
this.state = { kind: "idle" };
|
|
3336
4754
|
this.heartbeat.stop();
|
|
3337
4755
|
this.codec.reset();
|
|
3338
|
-
|
|
3339
|
-
|
|
4756
|
+
this.sendTail = Promise.resolve();
|
|
4757
|
+
try {
|
|
4758
|
+
await this.rtcManager.leaveRoom();
|
|
4759
|
+
} finally {
|
|
4760
|
+
this.leaveOperation = void 0;
|
|
4761
|
+
}
|
|
3340
4762
|
}
|
|
3341
4763
|
/**
|
|
3342
4764
|
* 组包入站消息并按目标用户过滤后分发完整业务消息。
|
|
@@ -3358,18 +4780,26 @@ var RoomController = class {
|
|
|
3358
4780
|
* 发送房间信令并输出日志。
|
|
3359
4781
|
*/
|
|
3360
4782
|
send(message) {
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
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);
|
|
3366
4791
|
}
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
() => `\u53D1\u9001\u623F\u95F4\u4FE1\u4EE4 (Outbound Room Signaling)
|
|
4792
|
+
XmaxLogger.room.info(
|
|
4793
|
+
() => `\u53D1\u9001\u623F\u95F4\u4FE1\u4EE4 (Outbound Room Signaling)
|
|
3370
4794
|
\u2514\u2500 ${XmaxLogger.localized("\u5185\u5BB9\uFF1A", "Content:")}
|
|
3371
4795
|
${JSON.stringify(JSON.parse(message), null, 2)}`
|
|
3372
|
-
|
|
4796
|
+
);
|
|
4797
|
+
}).catch((error) => {
|
|
4798
|
+
throw XmaxError.from(error);
|
|
4799
|
+
});
|
|
4800
|
+
this.sendTail = operation.catch(() => {
|
|
4801
|
+
});
|
|
4802
|
+
return operation;
|
|
3373
4803
|
}
|
|
3374
4804
|
/**
|
|
3375
4805
|
* 返回当前房间中的用户标识;未进房时抛出错误。
|
|
@@ -3542,6 +4972,9 @@ function makeInitialState() {
|
|
|
3542
4972
|
localVideoPublished: false,
|
|
3543
4973
|
localAudioPublished: false,
|
|
3544
4974
|
subscribedRemoteUserIDs: /* @__PURE__ */ new Set(),
|
|
4975
|
+
publishedRemoteUserIDs: /* @__PURE__ */ new Set(),
|
|
4976
|
+
remoteVideoTracks: /* @__PURE__ */ new Map(),
|
|
4977
|
+
remoteVideoSubscriptions: /* @__PURE__ */ new Map(),
|
|
3545
4978
|
subscribedRemoteAudioUserIDs: /* @__PURE__ */ new Set()
|
|
3546
4979
|
};
|
|
3547
4980
|
}
|
|
@@ -3595,6 +5028,10 @@ var StreamController = class {
|
|
|
3595
5028
|
});
|
|
3596
5029
|
this.generationTimeoutMs = options.generationTimeoutMs ?? 3e4;
|
|
3597
5030
|
this.rtcManager.setEventListener({
|
|
5031
|
+
onTokenWillExpire: options.onCredentialsRequired,
|
|
5032
|
+
onTokenExpired: options.onCredentialsRequired,
|
|
5033
|
+
onRoomRejoining: () => this.handleRoomRejoining(),
|
|
5034
|
+
onError: (error) => this.errorListener(error),
|
|
3598
5035
|
onNetworkStatistics: (statistics) => {
|
|
3599
5036
|
if (this.state.localVideoPublished) {
|
|
3600
5037
|
this.networkStatisticsListener?.(statistics);
|
|
@@ -3673,9 +5110,9 @@ var StreamController = class {
|
|
|
3673
5110
|
* @throws 连接已取消,或 RTC 进房、房间配置与本地流发布失败时抛出错误。
|
|
3674
5111
|
*/
|
|
3675
5112
|
async connect(connection, includeLocalAudio, ensureActive, beforePublish) {
|
|
5113
|
+
this.configureRoom({ roomID: connection.roomID, botID: connection.botID });
|
|
3676
5114
|
await this.roomController.join(connection, ensureActive);
|
|
3677
5115
|
ensureActive();
|
|
3678
|
-
this.configureRoom({ roomID: connection.roomID, botID: connection.botID });
|
|
3679
5116
|
XmaxLogger.stream.info(
|
|
3680
5117
|
() => `RTC \u623F\u95F4\u5DF2\u914D\u7F6E (RTC Room Configured)
|
|
3681
5118
|
\u2514\u2500 roomID: ${connection.roomID}, botID: ${connection.botID ?? "(\u672A\u8BBE\u7F6E)"}`
|
|
@@ -3691,12 +5128,18 @@ var StreamController = class {
|
|
|
3691
5128
|
await this.resetStream();
|
|
3692
5129
|
await this.roomController.leave();
|
|
3693
5130
|
}
|
|
5131
|
+
/**
|
|
5132
|
+
* 将会话刷新后的凭据应用到当前 RTC 连接。
|
|
5133
|
+
*/
|
|
5134
|
+
async updateCredentials(connection, signal) {
|
|
5135
|
+
await this.rtcManager.updateCredentials(toRoomJoinConfiguration(connection), signal);
|
|
5136
|
+
}
|
|
3694
5137
|
/**
|
|
3695
5138
|
* 建立生成任务并发送开始信令。
|
|
3696
5139
|
*
|
|
3697
5140
|
* @returns 等待远端结果流确认的 Promise。
|
|
3698
|
-
* @throws 任务标识无效、RTC
|
|
3699
|
-
*
|
|
5141
|
+
* @throws 任务标识无效、RTC 房间未就绪或已有生成任务时同步抛错;
|
|
5142
|
+
* 发送失败或确认超时通过返回的 Promise 拒绝。
|
|
3700
5143
|
*/
|
|
3701
5144
|
beginGeneration(options) {
|
|
3702
5145
|
const taskID = options.taskID.trim();
|
|
@@ -3738,24 +5181,44 @@ var StreamController = class {
|
|
|
3738
5181
|
};
|
|
3739
5182
|
this.state.generationTaskID = taskID;
|
|
3740
5183
|
this.state.generationWaiter = waiter;
|
|
5184
|
+
void confirmation.catch(() => {
|
|
5185
|
+
});
|
|
3741
5186
|
try {
|
|
3742
|
-
this.roomController.startGeneration({
|
|
5187
|
+
const sending = this.roomController.startGeneration({
|
|
3743
5188
|
taskID,
|
|
3744
5189
|
videoFormat: options.videoFormat,
|
|
3745
5190
|
targetSize: options.targetSize,
|
|
3746
5191
|
context: options.context
|
|
3747
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
|
+
});
|
|
3748
5211
|
} catch (error) {
|
|
3749
5212
|
const mapped = XmaxError.from(error);
|
|
3750
|
-
this.state.generationTaskID = void 0;
|
|
3751
5213
|
this.rejectGenerationStart(taskID, mapped);
|
|
5214
|
+
this.state.generationTaskID = void 0;
|
|
3752
5215
|
try {
|
|
3753
|
-
this.roomController.stopGeneration(taskID)
|
|
5216
|
+
void this.roomController.stopGeneration(taskID).catch(() => {
|
|
5217
|
+
});
|
|
3754
5218
|
} catch {
|
|
3755
5219
|
}
|
|
3756
5220
|
throw mapped;
|
|
3757
5221
|
}
|
|
3758
|
-
return confirmation;
|
|
3759
5222
|
}
|
|
3760
5223
|
/**
|
|
3761
5224
|
* 在远端首帧已经可显示后订阅当前生成流的音频。
|
|
@@ -3777,9 +5240,9 @@ var StreamController = class {
|
|
|
3777
5240
|
*
|
|
3778
5241
|
* @throws RTC 房间未就绪或条件变更信令发送失败时抛出错误。
|
|
3779
5242
|
*/
|
|
3780
|
-
updateGeneration(options) {
|
|
5243
|
+
async updateGeneration(options) {
|
|
3781
5244
|
try {
|
|
3782
|
-
this.roomController.changeGenerationCondition({
|
|
5245
|
+
await this.roomController.changeGenerationCondition({
|
|
3783
5246
|
taskID: options.taskID,
|
|
3784
5247
|
videoFormat: options.videoFormat,
|
|
3785
5248
|
targetSize: options.targetSize,
|
|
@@ -3794,9 +5257,9 @@ var StreamController = class {
|
|
|
3794
5257
|
*
|
|
3795
5258
|
* @throws 操作已取消、房间未就绪或信令发送失败时抛出错误。
|
|
3796
5259
|
*/
|
|
3797
|
-
changeTargetSize(taskID, targetSize, ensureActive) {
|
|
5260
|
+
async changeTargetSize(taskID, targetSize, ensureActive) {
|
|
3798
5261
|
try {
|
|
3799
|
-
this.roomController.changeTargetSize({ taskID, targetSize, ensureActive });
|
|
5262
|
+
await this.roomController.changeTargetSize({ taskID, targetSize, ensureActive });
|
|
3800
5263
|
} catch (error) {
|
|
3801
5264
|
throw XmaxError.from(error);
|
|
3802
5265
|
}
|
|
@@ -3812,7 +5275,7 @@ var StreamController = class {
|
|
|
3812
5275
|
return;
|
|
3813
5276
|
}
|
|
3814
5277
|
try {
|
|
3815
|
-
this.roomController.stopGeneration(stoppedTaskID);
|
|
5278
|
+
await this.roomController.stopGeneration(stoppedTaskID);
|
|
3816
5279
|
} catch (error) {
|
|
3817
5280
|
throw XmaxError.from(error);
|
|
3818
5281
|
}
|
|
@@ -3822,9 +5285,9 @@ var StreamController = class {
|
|
|
3822
5285
|
*
|
|
3823
5286
|
* @throws RTC 房间未就绪或轨迹信令发送失败时抛出错误。
|
|
3824
5287
|
*/
|
|
3825
|
-
sendTracks(taskID, points) {
|
|
5288
|
+
async sendTracks(taskID, points) {
|
|
3826
5289
|
try {
|
|
3827
|
-
this.roomController.sendTracks(taskID, points);
|
|
5290
|
+
await this.roomController.sendTracks(taskID, points);
|
|
3828
5291
|
} catch (error) {
|
|
3829
5292
|
throw XmaxError.from(error);
|
|
3830
5293
|
}
|
|
@@ -3899,6 +5362,12 @@ var StreamController = class {
|
|
|
3899
5362
|
if (!this.state.roomID) {
|
|
3900
5363
|
return;
|
|
3901
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
|
+
}
|
|
3902
5371
|
if (this.state.botID && this.state.botID !== trimmedUserID) {
|
|
3903
5372
|
if (this.state.generationTaskID === void 0) {
|
|
3904
5373
|
XmaxLogger.stream.warning(
|
|
@@ -3923,31 +5392,49 @@ var StreamController = class {
|
|
|
3923
5392
|
this.clearRemoteStream();
|
|
3924
5393
|
}
|
|
3925
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
|
+
}
|
|
3926
5406
|
/**
|
|
3927
5407
|
* 订阅远端视频并在成功后就绪或确认生成任务。
|
|
3928
5408
|
*/
|
|
3929
5409
|
async subscribeRemoteVideo(userID) {
|
|
3930
|
-
|
|
5410
|
+
const state2 = this.state;
|
|
5411
|
+
const existing = state2.remoteVideoTracks.get(userID);
|
|
5412
|
+
if (existing) {
|
|
5413
|
+
if (state2.generationWaiter) this.confirmOrUpdateRemoteStream(userID, existing);
|
|
3931
5414
|
return;
|
|
3932
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;
|
|
3933
5420
|
try {
|
|
3934
5421
|
const track = await this.rtcManager.subscribeRemoteVideo(userID, true);
|
|
5422
|
+
if (!isCurrent()) return;
|
|
3935
5423
|
if (!track) {
|
|
3936
5424
|
throw new XmaxError(
|
|
3937
5425
|
"MEDIA_ERROR" /* mediaError */,
|
|
3938
5426
|
"Remote video track is not available"
|
|
3939
5427
|
);
|
|
3940
5428
|
}
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
}
|
|
3944
|
-
this.state.subscribedRemoteUserIDs.add(userID);
|
|
5429
|
+
state2.subscribedRemoteUserIDs.add(userID);
|
|
5430
|
+
state2.remoteVideoTracks.set(userID, track);
|
|
3945
5431
|
XmaxLogger.stream.info(
|
|
3946
5432
|
() => `\u8FDC\u7AEF\u89C6\u9891\u8BA2\u9605\u6210\u529F (Remote Video Subscribed)
|
|
3947
5433
|
\u2514\u2500 userID: ${userID}`
|
|
3948
5434
|
);
|
|
3949
5435
|
this.confirmOrUpdateRemoteStream(userID, track);
|
|
3950
5436
|
} catch (error) {
|
|
5437
|
+
if (!isCurrent()) return;
|
|
3951
5438
|
const mapped = XmaxError.from(error);
|
|
3952
5439
|
const pendingTaskID = this.state.generationWaiter ? this.state.generationTaskID : void 0;
|
|
3953
5440
|
if (pendingTaskID) {
|
|
@@ -3955,6 +5442,8 @@ var StreamController = class {
|
|
|
3955
5442
|
} else {
|
|
3956
5443
|
this.errorListener(mapped);
|
|
3957
5444
|
}
|
|
5445
|
+
} finally {
|
|
5446
|
+
if (isCurrent()) state2.remoteVideoSubscriptions.delete(userID);
|
|
3958
5447
|
}
|
|
3959
5448
|
}
|
|
3960
5449
|
/**
|
|
@@ -3993,14 +5482,23 @@ var StreamController = class {
|
|
|
3993
5482
|
}
|
|
3994
5483
|
}
|
|
3995
5484
|
/**
|
|
3996
|
-
*
|
|
5485
|
+
* 订阅远端音频并按平台能力应用当前音量。
|
|
5486
|
+
*
|
|
5487
|
+
* 支持音量控制的平台先设音量再订阅,避免以默认音量短暂出声;
|
|
5488
|
+
* iOS 上音量降级为播放/静音二值控制,必须先完成订阅(恢复播放)
|
|
5489
|
+
* 再按需静音,否则订阅的恢复播放会覆盖音量 0 的静音状态。
|
|
3997
5490
|
*/
|
|
3998
5491
|
async subscribeRemoteAudio(userID) {
|
|
3999
5492
|
if (this.state.subscribedRemoteAudioUserIDs.has(userID)) {
|
|
4000
5493
|
return;
|
|
4001
5494
|
}
|
|
4002
|
-
this.rtcManager.
|
|
4003
|
-
|
|
5495
|
+
if (this.rtcManager.supportsRemoteAudioVolumeControl) {
|
|
5496
|
+
this.rtcManager.setRemoteAudioVolume(this.remoteAudioVolumePercentage, userID);
|
|
5497
|
+
await this.rtcManager.subscribeRemoteAudio(userID, true);
|
|
5498
|
+
} else {
|
|
5499
|
+
await this.rtcManager.subscribeRemoteAudio(userID, true);
|
|
5500
|
+
this.rtcManager.setRemoteAudioVolume(this.remoteAudioVolumePercentage, userID);
|
|
5501
|
+
}
|
|
4004
5502
|
this.state.subscribedRemoteAudioUserIDs.add(userID);
|
|
4005
5503
|
}
|
|
4006
5504
|
/**
|
|
@@ -4588,17 +6086,25 @@ var RealtimeLaunchTimer = class {
|
|
|
4588
6086
|
this.notify();
|
|
4589
6087
|
return complete;
|
|
4590
6088
|
}
|
|
6089
|
+
/**
|
|
6090
|
+
* 开始等待用户授权系统媒体权限,返回授权决定时调用的记录函数。
|
|
6091
|
+
* 仅在系统弹出授权请求时使用;已授权或未弹窗不记录该阶段。
|
|
6092
|
+
*/
|
|
6093
|
+
startPermission() {
|
|
6094
|
+
return this.measure("permissionMs", performance.now());
|
|
6095
|
+
}
|
|
4591
6096
|
/**
|
|
4592
6097
|
* 开始会话与进房计时,返回连接阶段完成时调用的记录函数。
|
|
6098
|
+
* 相机预热等待计入连接耗时。
|
|
4593
6099
|
*/
|
|
4594
6100
|
startConnection() {
|
|
4595
6101
|
return this.measure("connectionMs", performance.now());
|
|
4596
6102
|
}
|
|
4597
6103
|
/**
|
|
4598
|
-
*
|
|
6104
|
+
* 开始发布本地流计时,返回发布完成时调用的记录函数。
|
|
4599
6105
|
*/
|
|
4600
|
-
|
|
4601
|
-
return this.measure("
|
|
6106
|
+
startPublish() {
|
|
6107
|
+
return this.measure("publishMs", performance.now());
|
|
4602
6108
|
}
|
|
4603
6109
|
/**
|
|
4604
6110
|
* 开始等待远端显示首帧;完成回调同时记录首帧耗时和实际总耗时。
|
|
@@ -4654,6 +6160,10 @@ var XmaxRealtimeConnectionManager = class {
|
|
|
4654
6160
|
* 会话资源
|
|
4655
6161
|
*/
|
|
4656
6162
|
__publicField(this, "activeSession");
|
|
6163
|
+
__publicField(this, "refreshVersion", 0);
|
|
6164
|
+
__publicField(this, "credentialRefresh");
|
|
6165
|
+
__publicField(this, "credentialApplication", Promise.resolve());
|
|
6166
|
+
__publicField(this, "credentialController");
|
|
4657
6167
|
/**
|
|
4658
6168
|
* 远端轨道与渲染绑定
|
|
4659
6169
|
*/
|
|
@@ -4674,7 +6184,8 @@ var XmaxRealtimeConnectionManager = class {
|
|
|
4674
6184
|
}
|
|
4675
6185
|
/**
|
|
4676
6186
|
* 创建会话、配置编码、进房并发布本地媒体,启动心跳后返回远端占位流。
|
|
4677
|
-
* 进房结束即记录连接耗时;可选 beforePublish
|
|
6187
|
+
* 进房结束即记录连接耗时;可选 beforePublish 检查阻塞发布,计入连接耗时;
|
|
6188
|
+
* 发布本地流单独计时。
|
|
4678
6189
|
* @throws 会话、进房、发布失败或操作租约已失效时抛出错误,由上层统一清理。
|
|
4679
6190
|
*/
|
|
4680
6191
|
async connect(options) {
|
|
@@ -4692,6 +6203,11 @@ var XmaxRealtimeConnectionManager = class {
|
|
|
4692
6203
|
"Session does not contain RTC join information"
|
|
4693
6204
|
);
|
|
4694
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();
|
|
4695
6211
|
const videoFormat = options.localTrack.videoFormat;
|
|
4696
6212
|
if (!videoFormat) {
|
|
4697
6213
|
throw new XmaxError(
|
|
@@ -4701,27 +6217,29 @@ var XmaxRealtimeConnectionManager = class {
|
|
|
4701
6217
|
}
|
|
4702
6218
|
await streamController.setVideoEncoderConfig(videoFormat);
|
|
4703
6219
|
options.ensureCurrent();
|
|
6220
|
+
let completePublish;
|
|
4704
6221
|
await streamController.connect(connection, options.includeLocalAudio, () => {
|
|
4705
6222
|
options.ensureCurrent();
|
|
4706
6223
|
}, async () => {
|
|
4707
|
-
options.ensureCurrent();
|
|
4708
|
-
completeConnection();
|
|
4709
6224
|
options.ensureCurrent();
|
|
4710
6225
|
await options.beforePublish?.();
|
|
6226
|
+
completeConnection();
|
|
6227
|
+
completePublish = this.dependencies.timing.startPublish();
|
|
4711
6228
|
});
|
|
4712
6229
|
options.ensureCurrent();
|
|
6230
|
+
completePublish?.();
|
|
4713
6231
|
options.onPublished();
|
|
4714
6232
|
sessionService.startHeartbeat(session.id, {
|
|
4715
6233
|
onFailure: (sessionID, error) => {
|
|
4716
6234
|
this.dependencies.onHeartbeatFailure(sessionID, error);
|
|
4717
6235
|
},
|
|
4718
6236
|
onRefresh: (refreshed) => {
|
|
4719
|
-
this.handleSessionRefresh(refreshed);
|
|
6237
|
+
return this.handleSessionRefresh(refreshed, refreshVersion);
|
|
4720
6238
|
}
|
|
4721
6239
|
});
|
|
4722
6240
|
const remoteTrack = new RealtimeVideoTrack({
|
|
4723
6241
|
id: connection.botID ?? "video-remote",
|
|
4724
|
-
videoFormat: options.localTrack.videoFormat
|
|
6242
|
+
videoFormat: options.remoteVideoFormat ?? options.localTrack.videoFormat
|
|
4725
6243
|
});
|
|
4726
6244
|
this.activeRemoteTrack = remoteTrack;
|
|
4727
6245
|
this.registerRemoteBinding(remoteTrack);
|
|
@@ -4744,8 +6262,38 @@ var XmaxRealtimeConnectionManager = class {
|
|
|
4744
6262
|
* 停止会话心跳;未配置会话服务时不执行操作。
|
|
4745
6263
|
*/
|
|
4746
6264
|
stopHeartbeat() {
|
|
6265
|
+
this.refreshVersion++;
|
|
6266
|
+
this.credentialController?.abort();
|
|
6267
|
+
this.credentialController = void 0;
|
|
6268
|
+
this.credentialRefresh = void 0;
|
|
6269
|
+
this.credentialApplication = Promise.resolve();
|
|
4747
6270
|
this.dependencies.sessionService?.stopHeartbeat();
|
|
4748
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
|
+
}
|
|
4749
6297
|
/**
|
|
4750
6298
|
* 等待接收轨解除静音;超时只警告,取消必须立即退出并移除监听。
|
|
4751
6299
|
*/
|
|
@@ -4853,9 +6401,18 @@ var XmaxRealtimeConnectionManager = class {
|
|
|
4853
6401
|
* 心跳成功后的凭据刷新:仅凭据变化时覆盖本地缓存;房间绑定信息
|
|
4854
6402
|
* (提供方、房间、应用、登录身份)变化时当前连接失效,按失败结束。
|
|
4855
6403
|
*/
|
|
4856
|
-
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) {
|
|
4857
6414
|
const current = this.activeSession;
|
|
4858
|
-
if (!current || current.id !== session.id) {
|
|
6415
|
+
if (version !== this.refreshVersion || !current || current.id !== session.id) {
|
|
4859
6416
|
return;
|
|
4860
6417
|
}
|
|
4861
6418
|
const next = session.connection;
|
|
@@ -4863,7 +6420,7 @@ var XmaxRealtimeConnectionManager = class {
|
|
|
4863
6420
|
return;
|
|
4864
6421
|
}
|
|
4865
6422
|
const previous = current.connection;
|
|
4866
|
-
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);
|
|
4867
6424
|
if (bindingChanged) {
|
|
4868
6425
|
this.dependencies.onHeartbeatFailure(
|
|
4869
6426
|
session.id,
|
|
@@ -4871,6 +6428,10 @@ var XmaxRealtimeConnectionManager = class {
|
|
|
4871
6428
|
);
|
|
4872
6429
|
return;
|
|
4873
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
|
+
}
|
|
4874
6435
|
this.activeSession = new RealtimeSession({
|
|
4875
6436
|
id: session.id,
|
|
4876
6437
|
userID: session.userID ?? current.userID,
|
|
@@ -4983,16 +6544,17 @@ var XmaxRealtimeGenerationManager = class _XmaxRealtimeGenerationManager {
|
|
|
4983
6544
|
/**
|
|
4984
6545
|
* 更新活动任务的生成条件;信令发送成功后才覆盖缓存,无任务或上下文时抛错。
|
|
4985
6546
|
*/
|
|
4986
|
-
update(taskID, options) {
|
|
6547
|
+
async update(taskID, options) {
|
|
4987
6548
|
const context = options.context ?? this.currentContext;
|
|
4988
6549
|
if (!taskID || !context) {
|
|
4989
6550
|
throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "A realtime context is required to update the current generation");
|
|
4990
6551
|
}
|
|
4991
|
-
this.streamController.updateGeneration({
|
|
6552
|
+
await this.streamController.updateGeneration({
|
|
4992
6553
|
taskID,
|
|
4993
6554
|
videoFormat: options.videoFormat,
|
|
4994
6555
|
context
|
|
4995
6556
|
});
|
|
6557
|
+
if (this.activeTaskID !== taskID) throw RealtimeCoordinator.cancelledError();
|
|
4996
6558
|
this.currentContext = context;
|
|
4997
6559
|
}
|
|
4998
6560
|
/**
|
|
@@ -5080,6 +6642,10 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
5080
6642
|
*/
|
|
5081
6643
|
__publicField(this, "options");
|
|
5082
6644
|
__publicField(this, "mediaService");
|
|
6645
|
+
/**
|
|
6646
|
+
* 本地流创建时的生成格式;上行编码调整不改变模型输入配置与远端插帧尺寸。
|
|
6647
|
+
*/
|
|
6648
|
+
__publicField(this, "generationVideoFormat");
|
|
5083
6649
|
/**
|
|
5084
6650
|
* 插帧能力、用户配置与渲染版本
|
|
5085
6651
|
*/
|
|
@@ -5098,6 +6664,10 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
5098
6664
|
*/
|
|
5099
6665
|
__publicField(this, "launchTimer", new RealtimeLaunchTimer());
|
|
5100
6666
|
__publicField(this, "remoteFrameDisplayHandler");
|
|
6667
|
+
/**
|
|
6668
|
+
* 媒体权限弹窗观察
|
|
6669
|
+
*/
|
|
6670
|
+
__publicField(this, "permissionWatchDispose");
|
|
5101
6671
|
/**
|
|
5102
6672
|
* 本地、远端和网络统计快照及观察者
|
|
5103
6673
|
*/
|
|
@@ -5124,7 +6694,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
5124
6694
|
this.interpolationRequested = options.isFrameInterpolationEnabled;
|
|
5125
6695
|
this.supportsInterpolation = dependencies?.supportsFrameInterpolation ?? (async (size) => await frameInterpolationAdapter(size) !== null);
|
|
5126
6696
|
this.errorHandler = new RealtimeErrorHandler();
|
|
5127
|
-
const rtcManager = dependencies?.rtcManager ??
|
|
6697
|
+
const rtcManager = dependencies?.rtcManager ?? createRtcManager(options.provider, dependencies?.environment ?? "china" /* china */);
|
|
5128
6698
|
this.cameraController = dependencies?.cameraController ?? new CameraController({
|
|
5129
6699
|
rtcManager,
|
|
5130
6700
|
mediaService: this.mediaService,
|
|
@@ -5132,9 +6702,12 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
5132
6702
|
void this.errorHandler.report(error);
|
|
5133
6703
|
}
|
|
5134
6704
|
});
|
|
5135
|
-
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);
|
|
5136
6706
|
this.streamController = dependencies?.streamController ?? new StreamController({
|
|
5137
6707
|
rtcManager,
|
|
6708
|
+
onCredentialsRequired: () => {
|
|
6709
|
+
void this.connectionManager.refreshCredentials();
|
|
6710
|
+
},
|
|
5138
6711
|
errorListener: (error) => {
|
|
5139
6712
|
void this.errorHandler.report(error);
|
|
5140
6713
|
},
|
|
@@ -5144,6 +6717,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
5144
6717
|
}
|
|
5145
6718
|
});
|
|
5146
6719
|
this.connectionManager = new XmaxRealtimeConnectionManager({
|
|
6720
|
+
provider: options.provider,
|
|
5147
6721
|
sessionService,
|
|
5148
6722
|
streamController: this.streamController,
|
|
5149
6723
|
timing: this.launchTimer,
|
|
@@ -5199,7 +6773,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
5199
6773
|
*/
|
|
5200
6774
|
async setFrameInterpolationEnabled(enabled) {
|
|
5201
6775
|
await this.coordinator.run("configuration" /* configuration */, void 0, async (token) => {
|
|
5202
|
-
const format = this.cameraController.currentTrack?.videoFormat;
|
|
6776
|
+
const format = this.generationVideoFormat ?? this.cameraController.currentTrack?.videoFormat;
|
|
5203
6777
|
const size = format && enabled ? this.mediaService.resolveFrameInterpolationSize(format) : void 0;
|
|
5204
6778
|
if (enabled && !await this.checkInterpolationSupport(size, token)) {
|
|
5205
6779
|
token.ensureCurrent();
|
|
@@ -5416,7 +6990,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
5416
6990
|
*
|
|
5417
6991
|
* 将返回的轨道绑定到预览视图;收到有效帧且视图已绑定后进入 `ready`。
|
|
5418
6992
|
*
|
|
5419
|
-
* @param options.enableFrameValidation
|
|
6993
|
+
* @param options.enableFrameValidation 是否在发布前等待相机预热(固定 200ms),默认 true;false 跳过等待。
|
|
5420
6994
|
* @returns 包含本地相机视频轨道的媒体流。
|
|
5421
6995
|
* @throws 模型不支持相机输入、配置无效、权限或采集启动失败时抛出错误。
|
|
5422
6996
|
*/
|
|
@@ -5434,15 +7008,25 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
5434
7008
|
token.ensureCurrent();
|
|
5435
7009
|
const completeCamera = this.launchTimer.startCamera();
|
|
5436
7010
|
this.clearVideoStatistics();
|
|
7011
|
+
this.disposePermissionWatch();
|
|
7012
|
+
const permissionWatch = await watchMediaPermissionPrompt(options?.useMicrophone ?? false);
|
|
7013
|
+
if (permissionWatch) {
|
|
7014
|
+
const completePermission = this.launchTimer.startPermission();
|
|
7015
|
+
void permissionWatch.decided.then(() => completePermission()).catch(() => {
|
|
7016
|
+
});
|
|
7017
|
+
this.permissionWatchDispose = permissionWatch.dispose;
|
|
7018
|
+
}
|
|
5437
7019
|
let stream;
|
|
5438
7020
|
try {
|
|
5439
7021
|
token.ensureCurrent();
|
|
5440
7022
|
stream = await this.cameraController.createLocalCameraStream(options);
|
|
5441
7023
|
} catch (error) {
|
|
7024
|
+
this.disposePermissionWatch();
|
|
5442
7025
|
this.launchTimer.cancel();
|
|
5443
7026
|
throw error;
|
|
5444
7027
|
}
|
|
5445
7028
|
token.ensureCurrent();
|
|
7029
|
+
this.generationVideoFormat = stream.videoTrack?.videoFormat;
|
|
5446
7030
|
completeCamera();
|
|
5447
7031
|
this.cameraController.setPreviewReadyHandler((isCurrent) => {
|
|
5448
7032
|
void this.coordinator.localPreviewDidBecomeReady(isCurrent);
|
|
@@ -5451,6 +7035,33 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
5451
7035
|
}
|
|
5452
7036
|
);
|
|
5453
7037
|
}
|
|
7038
|
+
/**
|
|
7039
|
+
* 移除媒体权限弹窗观察;授权决定已记录或不再需要观察时调用。
|
|
7040
|
+
*/
|
|
7041
|
+
disposePermissionWatch() {
|
|
7042
|
+
this.permissionWatchDispose?.();
|
|
7043
|
+
this.permissionWatchDispose = void 0;
|
|
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
|
+
}
|
|
5454
7065
|
/**
|
|
5455
7066
|
* 停止本地相机流并释放本地预览与 RTC 资源。
|
|
5456
7067
|
*/
|
|
@@ -5461,8 +7072,10 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
5461
7072
|
async (token) => {
|
|
5462
7073
|
this.launchTimer.cancel();
|
|
5463
7074
|
this.clearVideoStatistics();
|
|
7075
|
+
this.disposePermissionWatch();
|
|
5464
7076
|
await this.cameraController.stopLocalCameraStream();
|
|
5465
7077
|
token.ensureCurrent();
|
|
7078
|
+
this.generationVideoFormat = void 0;
|
|
5466
7079
|
await this.coordinator.commit(
|
|
5467
7080
|
new RealtimeState({ connectionState: "Idle" /* idle */ }),
|
|
5468
7081
|
token
|
|
@@ -5494,8 +7107,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
5494
7107
|
* 使用当前 Manager 创建的本地流建立实时连接。
|
|
5495
7108
|
*
|
|
5496
7109
|
* 创建实时会话、加入 RTC 房间并发布本地流,成功后启动会话心跳。
|
|
5497
|
-
*
|
|
5498
|
-
* 记录警告并按当前画面继续发布,不因环境较暗而阻断连接。
|
|
7110
|
+
* 启用帧检测时,发布前并行等待相机预热(固定 200ms),避免把黑帧推给 RTC。
|
|
5499
7111
|
* 返回的远端媒体流在生成开始后承载远端生成画面。
|
|
5500
7112
|
*
|
|
5501
7113
|
* @param localStream 由 `createLocalCameraStream` 创建的本地媒体流。
|
|
@@ -5543,7 +7155,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
5543
7155
|
"The local stream must be created and started by this realtime manager"
|
|
5544
7156
|
);
|
|
5545
7157
|
}
|
|
5546
|
-
const videoFormat = localTrack.videoFormat;
|
|
7158
|
+
const videoFormat = this.generationVideoFormat ?? localTrack.videoFormat;
|
|
5547
7159
|
if (!videoFormat) {
|
|
5548
7160
|
throw new XmaxError(
|
|
5549
7161
|
"INVALID_CONFIGURATION" /* invalidConfiguration */,
|
|
@@ -5552,10 +7164,11 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
5552
7164
|
}
|
|
5553
7165
|
const currentState = this.coordinator.currentState;
|
|
5554
7166
|
if (currentState.connectionState === "Generating" /* generating */) {
|
|
5555
|
-
this.generationManager.update(currentState.taskID, {
|
|
7167
|
+
await this.generationManager.update(currentState.taskID, {
|
|
5556
7168
|
videoFormat,
|
|
5557
7169
|
context: options.context
|
|
5558
7170
|
});
|
|
7171
|
+
token.ensureCurrent();
|
|
5559
7172
|
return this.connectionManager.makeRemoteStream();
|
|
5560
7173
|
}
|
|
5561
7174
|
const context = this.generationManager.validateContext(options.context);
|
|
@@ -5604,17 +7217,15 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
5604
7217
|
new RealtimeState({ connectionState: "Connecting" /* connecting */ }),
|
|
5605
7218
|
token
|
|
5606
7219
|
);
|
|
5607
|
-
const
|
|
5608
|
-
const
|
|
5609
|
-
token.signal.addEventListener("abort",
|
|
5610
|
-
if (token.signal.aborted)
|
|
7220
|
+
const warmupController = new AbortController();
|
|
7221
|
+
const abortWarmup = () => warmupController.abort();
|
|
7222
|
+
token.signal.addEventListener("abort", abortWarmup, { once: true });
|
|
7223
|
+
if (token.signal.aborted) abortWarmup();
|
|
5611
7224
|
try {
|
|
5612
7225
|
let beforePublish;
|
|
5613
7226
|
if (this.cameraController.isFrameValidationEnabled) {
|
|
5614
|
-
const
|
|
5615
|
-
const cameraReady = this.cameraController.waitForValidCameraFrame(exposureController.signal).then(() => {
|
|
7227
|
+
const cameraReady = this.cameraController.waitForValidCameraFrame(warmupController.signal).then(() => {
|
|
5616
7228
|
token.ensureCurrent();
|
|
5617
|
-
completeFrameValidation();
|
|
5618
7229
|
});
|
|
5619
7230
|
void cameraReady.catch(() => {
|
|
5620
7231
|
});
|
|
@@ -5622,6 +7233,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
5622
7233
|
}
|
|
5623
7234
|
const remote = await this.connectionManager.connect({
|
|
5624
7235
|
localTrack,
|
|
7236
|
+
remoteVideoFormat: this.generationVideoFormat,
|
|
5625
7237
|
model: this.options.model,
|
|
5626
7238
|
includeLocalAudio: this.cameraController.useMicrophone,
|
|
5627
7239
|
ensureCurrent: () => token.ensureCurrent(),
|
|
@@ -5639,8 +7251,8 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
5639
7251
|
);
|
|
5640
7252
|
return remote;
|
|
5641
7253
|
} finally {
|
|
5642
|
-
token.signal.removeEventListener("abort",
|
|
5643
|
-
|
|
7254
|
+
token.signal.removeEventListener("abort", abortWarmup);
|
|
7255
|
+
abortWarmup();
|
|
5644
7256
|
}
|
|
5645
7257
|
}
|
|
5646
7258
|
/**
|
|
@@ -5697,6 +7309,8 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
5697
7309
|
await this.generationManager.reset(taskID);
|
|
5698
7310
|
const sessionID = await this.connectionManager.disconnect();
|
|
5699
7311
|
if (scope === 1 /* all */) {
|
|
7312
|
+
this.generationVideoFormat = void 0;
|
|
7313
|
+
this.disposePermissionWatch();
|
|
5700
7314
|
try {
|
|
5701
7315
|
await this.cameraController.stopLocalCameraStream();
|
|
5702
7316
|
} catch (error) {
|
|
@@ -5802,12 +7416,18 @@ var XmaxClient = class {
|
|
|
5802
7416
|
}
|
|
5803
7417
|
/**
|
|
5804
7418
|
* 创建实时媒体 Manager。
|
|
7419
|
+
* 模型专用地址优先于环境默认地址,并用于该 Manager 的整个会话生命周期。
|
|
5805
7420
|
*
|
|
5806
7421
|
* @param options 实时生成模型等业务配置。
|
|
5807
7422
|
* @returns 可用于本地相机预览、实时连接与生成的实时 Manager。
|
|
5808
7423
|
*/
|
|
5809
7424
|
createRealtimeManager(options) {
|
|
5810
|
-
|
|
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
|
+
});
|
|
5811
7431
|
}
|
|
5812
7432
|
/**
|
|
5813
7433
|
* 创建媒体处理与能力查询 Service。
|
|
@@ -5833,10 +7453,15 @@ var RealtimeConfiguration = class {
|
|
|
5833
7453
|
* 创建实时业务配置。
|
|
5834
7454
|
*
|
|
5835
7455
|
* @param init.model 实时生成业务使用的模型。
|
|
7456
|
+
* @param init.provider RTC 提供方,默认 TRTC;创建后不可切换。
|
|
5836
7457
|
* @param init.isFrameInterpolationEnabled 是否默认开启远端生成画面的插帧。
|
|
5837
7458
|
* @param init.frameInterpolation 插帧开关和显示帧率上限的详细配置。
|
|
5838
7459
|
*/
|
|
5839
7460
|
constructor(init) {
|
|
7461
|
+
/**
|
|
7462
|
+
* RTC 配置
|
|
7463
|
+
*/
|
|
7464
|
+
__publicField(this, "provider");
|
|
5840
7465
|
/**
|
|
5841
7466
|
* 模型配置
|
|
5842
7467
|
*/
|
|
@@ -5855,6 +7480,10 @@ var RealtimeConfiguration = class {
|
|
|
5855
7480
|
* 已补全默认值的只读插帧配置。
|
|
5856
7481
|
*/
|
|
5857
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
|
+
}
|
|
5858
7487
|
this.model = init.model;
|
|
5859
7488
|
const targetFrameRate = init.frameInterpolation?.targetFrameRate ?? 60;
|
|
5860
7489
|
if (!Number.isFinite(targetFrameRate) || targetFrameRate < 1 || targetFrameRate > 60) {
|
|
@@ -8702,6 +10331,7 @@ export {
|
|
|
8702
10331
|
StoredFile,
|
|
8703
10332
|
StorageService,
|
|
8704
10333
|
CameraPosition,
|
|
10334
|
+
RtcProvider,
|
|
8705
10335
|
RealtimeVideoTrack,
|
|
8706
10336
|
RealtimeMediaStream,
|
|
8707
10337
|
RealtimeConnectionState,
|
|
@@ -8715,4 +10345,4 @@ export {
|
|
|
8715
10345
|
XmaxVideoView,
|
|
8716
10346
|
XmaxRealtimeVideoView
|
|
8717
10347
|
};
|
|
8718
|
-
//# sourceMappingURL=chunk-
|
|
10348
|
+
//# sourceMappingURL=chunk-FMAQXPTE.js.map
|