@xmov/avatar 2.0.0-beta.14 → 2.0.0-beta.16
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/dist/agent/cdn-entry.d.ts +1 -0
- package/dist/control/RenderScheduler.d.ts +9 -0
- package/dist/control/ttsa.d.ts +8 -1
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.modern.js +1 -1
- package/dist/index.modern.js.map +1 -1
- package/dist/index.module.js +1 -1
- package/dist/index.module.js.map +1 -1
- package/dist/index.umd.js +1 -1
- package/dist/index.umd.js.map +1 -1
- package/dist/utils/request.d.ts +1 -0
- package/package.json +6 -3
- package/src/agent/cdn-entry.ts +15 -0
- package/src/control/RenderScheduler.ts +15 -0
- package/src/control/ttsa.ts +47 -11
- package/src/index.ts +40 -38
- package/src/utils/request.ts +14 -2
package/dist/utils/request.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xmov/avatar",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
3
|
+
"version": "2.0.0-beta.16",
|
|
4
4
|
"description": "魔珐数字人 JS-SDK",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"source": "src/index.ts",
|
|
@@ -26,8 +26,10 @@
|
|
|
26
26
|
"src/"
|
|
27
27
|
],
|
|
28
28
|
"scripts": {
|
|
29
|
-
"build": "
|
|
29
|
+
"build": "pnpm run build:core && pnpm run build:agent && pnpm run build:e2e:cdn && pnpm run test:e2e:cdn && node upload-sdk.js build",
|
|
30
|
+
"build:core": "microbundle --workers --define ENV=production,VERSION=$npm_package_version --external none --tsconfig tsconfig.json",
|
|
30
31
|
"build:agent": "microbundle --entry src/agent/index.ts --output dist/agent --format modern,esm,cjs,umd --external @xmov/avatar --alias ../index=@xmov/avatar --globals @xmov/avatar=XmovAvatar --name XingyunAvatarAgentModule --tsconfig tsconfig.agent.json && node scripts/build-agent-entry.mjs",
|
|
32
|
+
"build:e2e:cdn": "microbundle --entry src/agent/cdn-entry.ts --output cdn-dist/xmovAvatar_e2e.js --format iife --external none --workers --define ENV=production,VERSION=$npm_package_version --tsconfig tsconfig.agent.json --no-pkg-main --no-sourcemap",
|
|
31
33
|
"sdk": "pnpm --filter @xmov/avatar run --parallel --no-bail \"/^sdk:.+$/\"",
|
|
32
34
|
"sdk:core": "microbundle watch --workers --define ENV=development,VERSION=$npm_package_version --external none --tsconfig tsconfig.json",
|
|
33
35
|
"sdk:agent": "node scripts/build-agent-entry.mjs && microbundle watch --entry src/agent/index.ts --output dist/agent --format modern,esm,cjs,umd --external @xmov/avatar --alias ../index=@xmov/avatar --globals @xmov/avatar=XmovAvatar --name XingyunAvatarAgentModule --tsconfig tsconfig.agent.json",
|
|
@@ -36,7 +38,8 @@
|
|
|
36
38
|
"test:agent": "node scripts/run-agent-tests.mjs",
|
|
37
39
|
"test:agent:exports": "pnpm run build:agent && node scripts/test-agent-package-exports.mjs",
|
|
38
40
|
"test:agent:browser": "playwright test",
|
|
39
|
-
"
|
|
41
|
+
"test:e2e:cdn": "node scripts/test-e2e-cdn-build.mjs",
|
|
42
|
+
"lite": "node replaceString.js && pnpm run build:core && node upload-sdk.js lite",
|
|
40
43
|
"proto": "pbjs -t static-module -w closure -o src/proto/face_data_pb.js src/proto/face_data.proto",
|
|
41
44
|
"commitlint": "commitlint --edit",
|
|
42
45
|
"prepare": "husky"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import XingyunAvatarAgent, {
|
|
2
|
+
AgentE2EClient,
|
|
3
|
+
DEFAULT_AGENT_AUDIO,
|
|
4
|
+
MicrophoneController,
|
|
5
|
+
} from "./index";
|
|
6
|
+
|
|
7
|
+
Object.assign(XingyunAvatarAgent, {
|
|
8
|
+
AgentE2EClient,
|
|
9
|
+
DEFAULT_AGENT_AUDIO,
|
|
10
|
+
MicrophoneController,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
(globalThis as typeof globalThis & {
|
|
14
|
+
XingyunAvatarAgent: typeof XingyunAvatarAgent;
|
|
15
|
+
}).XingyunAvatarAgent = XingyunAvatarAgent;
|
|
@@ -840,6 +840,21 @@ export default class RenderScheduler {
|
|
|
840
840
|
}])
|
|
841
841
|
}
|
|
842
842
|
|
|
843
|
+
/**
|
|
844
|
+
* 重连成功恢复(含窗口内 socket.io 自动重连恢复、窗口外重开房间):
|
|
845
|
+
* 数据缓存在断网进入离线时已清空(_offlineMode/_offlineRun),此处仅恢复播放状态:
|
|
846
|
+
* - 标记帧号对齐:首个 BODY/AUDIO 数据到达时按 sf 重设帧时钟
|
|
847
|
+
* - 恢复音频播放
|
|
848
|
+
* - 解码器退出离线模式(重置 _offlineLastFrame、清空离线队列)
|
|
849
|
+
* 避免离线 idle 播放推进的帧号领先服务端续推数据导致人物卡死、音画不同步
|
|
850
|
+
*/
|
|
851
|
+
markReconnectRecover() {
|
|
852
|
+
// 恢复音频播放
|
|
853
|
+
this.audioRenderer.resume();
|
|
854
|
+
// 解码器退出离线模式(重置 _offlineLastFrame、清空离线队列)
|
|
855
|
+
this.decoder._reload();
|
|
856
|
+
}
|
|
857
|
+
|
|
843
858
|
_offlineMode() {
|
|
844
859
|
this.dataCacheQueue._clearEvent();
|
|
845
860
|
const currentFrame = this.getCurrentFrameIndex() ?? 0;
|
package/src/control/ttsa.ts
CHANGED
|
@@ -55,6 +55,7 @@ interface ITtsaOptions {
|
|
|
55
55
|
enterOfflineMode: () => void;
|
|
56
56
|
reStartSDK: () => void;
|
|
57
57
|
reconnect_client_timeout: number;
|
|
58
|
+
reconnect_timeout: number;
|
|
58
59
|
sendVoiceEnd: () => void;
|
|
59
60
|
getCurrentFrameIndex: () => number;
|
|
60
61
|
}
|
|
@@ -77,7 +78,9 @@ export default class Ttsa {
|
|
|
77
78
|
private enterOfflineMode: () => void;
|
|
78
79
|
private reStartSDK: () => void;
|
|
79
80
|
private reconnect_client_timeout: number;
|
|
80
|
-
private
|
|
81
|
+
private reconnect_timeout: number;
|
|
82
|
+
private reconnectTimer = -1; // 重开房间定时器(reconnect_client_timeout 后 stopSession + reStartSDK)
|
|
83
|
+
private socketReconnectTimer = -1; // 停止 socket.io 自动重连定时器(reconnect_timeout 后旧房间已失效)
|
|
81
84
|
private wsConnectTimer = 0;
|
|
82
85
|
private enterOfflineFlag = false;
|
|
83
86
|
private sendVoiceEnd: () => void;
|
|
@@ -94,13 +97,26 @@ export default class Ttsa {
|
|
|
94
97
|
private readonly HEARTBEAT_INTERVAL = 2000;
|
|
95
98
|
|
|
96
99
|
/**
|
|
97
|
-
* 清除WebSocket
|
|
100
|
+
* 清除WebSocket重连定时器(含停止 socket.io 自动重连的定时器)
|
|
98
101
|
*/
|
|
99
102
|
public clearReconnectTimer(): void {
|
|
100
103
|
if (this.reconnectTimer !== -1) {
|
|
101
104
|
clearTimeout(this.reconnectTimer);
|
|
102
105
|
this.reconnectTimer = -1;
|
|
103
106
|
}
|
|
107
|
+
// 一并清理停止 socket.io 自动重连的定时器:网络恢复后交由 socket.io 自行重连,
|
|
108
|
+
// 不再强制断开(下次 disconnect 会重新按窗口设置)
|
|
109
|
+
this.clearSocketReconnectTimer();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 清除停止 socket.io 自动重连的定时器
|
|
114
|
+
*/
|
|
115
|
+
private clearSocketReconnectTimer(): void {
|
|
116
|
+
if (this.socketReconnectTimer !== -1) {
|
|
117
|
+
clearTimeout(this.socketReconnectTimer);
|
|
118
|
+
this.socketReconnectTimer = -1;
|
|
119
|
+
}
|
|
104
120
|
}
|
|
105
121
|
|
|
106
122
|
/**
|
|
@@ -146,6 +162,7 @@ export default class Ttsa {
|
|
|
146
162
|
this.enterOfflineMode = options.enterOfflineMode;
|
|
147
163
|
this.reStartSDK = options.reStartSDK;
|
|
148
164
|
this.reconnect_client_timeout = options.reconnect_client_timeout;
|
|
165
|
+
this.reconnect_timeout = options.reconnect_timeout;
|
|
149
166
|
this.sendVoiceEnd = options.sendVoiceEnd;
|
|
150
167
|
this.getCurrentFrameIndex = options.getCurrentFrameIndex;
|
|
151
168
|
|
|
@@ -154,6 +171,9 @@ export default class Ttsa {
|
|
|
154
171
|
token: this.token,
|
|
155
172
|
},
|
|
156
173
|
transports: ['websocket'],
|
|
174
|
+
// 自动重连不限制次数:在 reconnect_timeout(服务端断开旧房间的窗口)内持续尝试,
|
|
175
|
+
// 失效房间必然重连失败;超过窗口由 disconnect 处理器中的停止定时器断开,
|
|
176
|
+
// 超过 reconnect_client_timeout 由重开房间定时器兜底(stopSession + reStartSDK)
|
|
157
177
|
reconnection: true,
|
|
158
178
|
reconnectionAttempts: Infinity,
|
|
159
179
|
reconnectionDelay: 1000,
|
|
@@ -167,17 +187,21 @@ export default class Ttsa {
|
|
|
167
187
|
this.initWsTimeoutTimer();
|
|
168
188
|
// 启动客户端心跳
|
|
169
189
|
this.startHeartbeat();
|
|
190
|
+
// 重置离线处理标志:重连成功后允许下次断网正常进入离线/重连流程
|
|
191
|
+
this.enterOfflineFlag = false;
|
|
170
192
|
(window as any).performanceTracker.markEnd(performanceConstant.ttsa_connect);
|
|
171
193
|
const payload = {
|
|
172
194
|
room: this.room,
|
|
173
195
|
client_type: "web",
|
|
174
196
|
invisible_mode: this.sdk.getPendingInvisibleMode(),
|
|
175
197
|
};
|
|
176
|
-
// TODO: 临时处理,待后端修复
|
|
177
|
-
if(this.sdk.getStatus()=== AvatarStatus.offline) {
|
|
178
|
-
this.sdk.onStatusChange(AvatarStatus.online);
|
|
179
|
-
}
|
|
180
198
|
this.send("enter_room", payload);
|
|
199
|
+
// 窗口内自动重连恢复:服务端可能不重发 first_start_timestamp,
|
|
200
|
+
// 若仅依赖 reloadSuccess 补发 online/恢复播放,状态会卡在离线导致重连后卡死。
|
|
201
|
+
// 因此重连成功即恢复(仅初始化完成且此前已离线时;初次启动 status 为 -1 不受影响)
|
|
202
|
+
if (this.sdk.getStatus() === AvatarStatus.offline && this.sdk.getIsInitialized()) {
|
|
203
|
+
this.reloadSuccess();
|
|
204
|
+
}
|
|
181
205
|
});
|
|
182
206
|
ws.on("first_start_timestamp", (e: any) => {
|
|
183
207
|
(window as any).avatarSDKLogger.log(this.TAG, "first_start_timestamp", e);
|
|
@@ -339,11 +363,23 @@ export default class Ttsa {
|
|
|
339
363
|
// 无网断开则进入离线模式
|
|
340
364
|
this.enterOfflineMode();
|
|
341
365
|
}
|
|
342
|
-
//
|
|
343
|
-
//
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
366
|
+
// 重连窗口策略(不再依赖 navigator.onLine,socket 断开即启动):
|
|
367
|
+
// ① socketReconnectTimer(reconnect_timeout):窗口内 socket.io 自动重连;
|
|
368
|
+
// 服务端会在 reconnect_timeout 后断开旧房间,因此窗口结束即停止无谓的自动重连
|
|
369
|
+
// ② reconnectTimer(reconnect_client_timeout):仍未能重连则停止旧连接并重开房间
|
|
370
|
+
// 均仅首次设置,避免 socket.io 重连失败反复触发 disconnect 重置定时器导致永不执行;
|
|
371
|
+
// 重连成功时 connect 处理器、网络恢复时 NetworkMonitor -> clearReconnectTimer 都会取消
|
|
372
|
+
if (this.socketReconnectTimer === -1 && this.reconnect_timeout > 0) {
|
|
373
|
+
this.socketReconnectTimer = window.setTimeout(() => {
|
|
374
|
+
try {
|
|
375
|
+
ws.disconnect();
|
|
376
|
+
} catch (error) {
|
|
377
|
+
(window as any).avatarSDKLogger.warn(this.TAG, "disconnect error", error);
|
|
378
|
+
}
|
|
379
|
+
this.socketReconnectTimer = -1;
|
|
380
|
+
}, this.reconnect_timeout * 1000);
|
|
381
|
+
}
|
|
382
|
+
if (this.reconnectTimer === -1) {
|
|
347
383
|
this.reconnectTimer = window.setTimeout(async () => {
|
|
348
384
|
try {
|
|
349
385
|
ws.disconnect();
|
package/src/index.ts
CHANGED
|
@@ -104,7 +104,6 @@ export default class XmovAvatar {
|
|
|
104
104
|
private pcmCaptureMode: 'webcodec' | 'mediarecorder' | 'none' = 'none';
|
|
105
105
|
private pcmEncodeId: number = 0; // 用于 reset 竞态检测,递增标识当前编码会话
|
|
106
106
|
|
|
107
|
-
private reconnectDebounceTimer = -1; // 重连防抖定时器
|
|
108
107
|
private networkInfo: any = {
|
|
109
108
|
rtt: 0,
|
|
110
109
|
downlink: 0.0,
|
|
@@ -200,6 +199,14 @@ export default class XmovAvatar {
|
|
|
200
199
|
return this.status
|
|
201
200
|
}
|
|
202
201
|
|
|
202
|
+
/**
|
|
203
|
+
* 初始化是否完成(progress === 100)
|
|
204
|
+
* 供 TTSA 重连恢复时判断是否可提前恢复在线状态
|
|
205
|
+
*/
|
|
206
|
+
public getIsInitialized() {
|
|
207
|
+
return this.isInitialized
|
|
208
|
+
}
|
|
209
|
+
|
|
203
210
|
public get businessENV() {
|
|
204
211
|
return this._env
|
|
205
212
|
}
|
|
@@ -294,6 +301,10 @@ export default class XmovAvatar {
|
|
|
294
301
|
// 标记初始化完成
|
|
295
302
|
this.isInitialized = progress === 100;
|
|
296
303
|
if(progress === 100) {
|
|
304
|
+
// 初始化完成:若 start 时因未初始化完成而未上线(状态守卫),此处补发 online
|
|
305
|
+
if (!this.pendingInvisibleMode && this.status !== AvatarStatus.online && this.status !== AvatarStatus.invisible) {
|
|
306
|
+
this.onStatusChange(AvatarStatus.online);
|
|
307
|
+
}
|
|
297
308
|
this.resourceManager._setOfflineCache();
|
|
298
309
|
this.ttsa?.sendSdkPoint('loadvideo_decode_time', {
|
|
299
310
|
timestamp: Date.now(),
|
|
@@ -1103,6 +1114,7 @@ export default class XmovAvatar {
|
|
|
1103
1114
|
token: sessionInfo.token,
|
|
1104
1115
|
framedata_proto_version: this.resourceManager.getConfig().framedata_proto_version,
|
|
1105
1116
|
reconnect_client_timeout: sessionInfo?.reconnect_client_timeout || 0,
|
|
1117
|
+
reconnect_timeout: sessionInfo?.reconnect_timeout || 0,
|
|
1106
1118
|
appInfo: {
|
|
1107
1119
|
...this.resourceManager.getAppInfo(),
|
|
1108
1120
|
env: this._env
|
|
@@ -1111,7 +1123,7 @@ export default class XmovAvatar {
|
|
|
1111
1123
|
this.ttsa?.sendSdkPoint('connect_sdk', {
|
|
1112
1124
|
timestamp: this.startConnectTime,
|
|
1113
1125
|
});
|
|
1114
|
-
|
|
1126
|
+
// 不再强行上报 85:进度以真实资源下载为准,避免重连时 UI 进度被错误拉低
|
|
1115
1127
|
this.ttsa?.sendSdkPoint('connect_sdk_success', {
|
|
1116
1128
|
timestamp: this.connectSuccessTime,
|
|
1117
1129
|
});
|
|
@@ -1165,16 +1177,18 @@ export default class XmovAvatar {
|
|
|
1165
1177
|
}
|
|
1166
1178
|
|
|
1167
1179
|
protected start() {
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
// 如果初始化时设置了隐身模式,则立即进入隐身状态
|
|
1180
|
+
// 先发布在线状态,再启动渲染:
|
|
1181
|
+
// 避免先 render() 触发 rendering -> visible(5) 后再发 online(0),造成 0/5 状态横跳
|
|
1172
1182
|
if (this.pendingInvisibleMode) {
|
|
1173
1183
|
this.pendingInvisibleMode = false;
|
|
1174
1184
|
this.setInvisibleMode()
|
|
1175
|
-
} else {
|
|
1185
|
+
} else if (this.isInitialized) {
|
|
1186
|
+
// 状态守卫:仅在初始化完成(progress === 100)时上线,
|
|
1187
|
+
// 避免重连重建房间过程中(资源未下载完)反复触发 online -> visible 状态横跳
|
|
1176
1188
|
this.onStatusChange(AvatarStatus.online);
|
|
1177
1189
|
}
|
|
1190
|
+
this.renderScheduler.render();
|
|
1191
|
+
this.ttsa?.start();
|
|
1178
1192
|
|
|
1179
1193
|
}
|
|
1180
1194
|
|
|
@@ -1221,7 +1235,14 @@ export default class XmovAvatar {
|
|
|
1221
1235
|
}
|
|
1222
1236
|
|
|
1223
1237
|
reloadSuccess() {
|
|
1224
|
-
|
|
1238
|
+
// 重连成功恢复:恢复音频、重置解码器离线状态、清空旧数据缓存,并按首个数据 sf 对齐帧号,
|
|
1239
|
+
// 解决窗口内自动重连恢复后人物卡死(帧号错位数据被过滤)、无声音/音画不同步的问题
|
|
1240
|
+
this.renderScheduler?.markReconnectRecover();
|
|
1241
|
+
// 状态守卫:仅初始化完成时上线;未完成时由 onDownloadProgress(100) 补发,
|
|
1242
|
+
// 避免重连重建房间过程中初始化未完成却提前进入 online
|
|
1243
|
+
if (this.isInitialized) {
|
|
1244
|
+
this.onStatusChange(AvatarStatus.online);
|
|
1245
|
+
}
|
|
1225
1246
|
setTimeout(() => {
|
|
1226
1247
|
window.clearInterval(this._offlineTimer)
|
|
1227
1248
|
}, 1000)
|
|
@@ -1636,28 +1657,13 @@ export default class XmovAvatar {
|
|
|
1636
1657
|
}
|
|
1637
1658
|
|
|
1638
1659
|
private triggerReconnect() {
|
|
1639
|
-
//
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
clearTimeout(this.reconnectDebounceTimer);
|
|
1647
|
-
this.reconnectDebounceTimer = -1;
|
|
1648
|
-
}
|
|
1649
|
-
if (this.retryTimer !== -1) {
|
|
1650
|
-
clearTimeout(this.retryTimer);
|
|
1651
|
-
this.retryTimer = -1;
|
|
1652
|
-
}
|
|
1653
|
-
|
|
1654
|
-
this.reconnectDebounceTimer = window.setTimeout(() => {
|
|
1655
|
-
this.isRetrying = false;
|
|
1656
|
-
this.retryCount = 1;
|
|
1657
|
-
this.retryRound = 1;
|
|
1658
|
-
this.reStartSession();
|
|
1659
|
-
this.reconnectDebounceTimer = -1;
|
|
1660
|
-
}, 100);
|
|
1660
|
+
// 网络恢复触发:无论是否正在退避重试,都取消挂起的重试定时器并立即重连(重开房间),
|
|
1661
|
+
// 避免网络恢复后仍需等待当前退避窗口(最长 32s)才重连
|
|
1662
|
+
this.clearAllRetryTimers();
|
|
1663
|
+
this.isRetrying = false;
|
|
1664
|
+
this.retryCount = 1;
|
|
1665
|
+
this.retryRound = 1;
|
|
1666
|
+
this.reStartSession();
|
|
1661
1667
|
}
|
|
1662
1668
|
|
|
1663
1669
|
reStartSession() {
|
|
@@ -1683,8 +1689,9 @@ export default class XmovAvatar {
|
|
|
1683
1689
|
delay = Math.pow(2, this.retryCount) * 1000;
|
|
1684
1690
|
}
|
|
1685
1691
|
|
|
1686
|
-
// 使用setTimeout
|
|
1687
|
-
setTimeout(async () => {
|
|
1692
|
+
// 使用setTimeout代替递归,避免调用栈溢出;记录句柄以便网络恢复时取消挂起的退避重试
|
|
1693
|
+
this.retryTimer = window.setTimeout(async () => {
|
|
1694
|
+
this.retryTimer = -1;
|
|
1688
1695
|
if (!this.isRetrying || this.destroyed) {
|
|
1689
1696
|
return;
|
|
1690
1697
|
}
|
|
@@ -1759,12 +1766,7 @@ export default class XmovAvatar {
|
|
|
1759
1766
|
}, delay);
|
|
1760
1767
|
}
|
|
1761
1768
|
private clearAllRetryTimers() {
|
|
1762
|
-
//
|
|
1763
|
-
if (this.reconnectDebounceTimer !== -1) {
|
|
1764
|
-
clearTimeout(this.reconnectDebounceTimer);
|
|
1765
|
-
this.reconnectDebounceTimer = -1;
|
|
1766
|
-
}
|
|
1767
|
-
// 清理重试定时器
|
|
1769
|
+
// 清理重试定时器(含退避重试挂起的定时器)
|
|
1768
1770
|
if (this.retryTimer !== -1) {
|
|
1769
1771
|
clearTimeout(this.retryTimer);
|
|
1770
1772
|
this.retryTimer = -1;
|
package/src/utils/request.ts
CHANGED
|
@@ -70,20 +70,32 @@ export function XMLRequest(options: {
|
|
|
70
70
|
cache?: {
|
|
71
71
|
enable: boolean
|
|
72
72
|
}
|
|
73
|
+
timeout?: number
|
|
73
74
|
onProgress: (progress: number, e: ProgressEvent) => void
|
|
74
75
|
}): Promise<[boolean, ArrayBuffer | string]> {
|
|
75
76
|
return new Promise((resolve, reject) => {
|
|
76
77
|
const xhr = new XMLHttpRequest();
|
|
77
78
|
xhr.open('GET', options.url);
|
|
78
79
|
xhr.responseType = 'arraybuffer';
|
|
80
|
+
// 超时兜底:断网/请求挂起时不再永久等待(默认 60s,资源下载可能超时失败)
|
|
81
|
+
xhr.timeout = options.timeout ?? 60000;
|
|
79
82
|
xhr.onload = function() {
|
|
80
83
|
if (xhr.readyState === 4 && xhr.status === 200) {
|
|
81
84
|
const arrayBuffer = xhr.response;
|
|
82
85
|
resolve([false, arrayBuffer]);
|
|
83
|
-
} else
|
|
84
|
-
reject([true, xhr.statusText]);
|
|
86
|
+
} else {
|
|
87
|
+
reject([true, `HTTP ${xhr.status} ${xhr.statusText}`]);
|
|
85
88
|
}
|
|
86
89
|
};
|
|
90
|
+
xhr.onerror = function() {
|
|
91
|
+
reject([true, 'network error']);
|
|
92
|
+
};
|
|
93
|
+
xhr.ontimeout = function() {
|
|
94
|
+
reject([true, `timeout after ${xhr.timeout}ms`]);
|
|
95
|
+
};
|
|
96
|
+
xhr.onabort = function() {
|
|
97
|
+
reject([true, 'aborted']);
|
|
98
|
+
};
|
|
87
99
|
xhr.onprogress = (e) => {
|
|
88
100
|
if (e.lengthComputable) {
|
|
89
101
|
const percent = Math.round((e.loaded / e.total) * 100);
|