@xmov/avatar 2.0.0-alpha.64 → 2.0.0-alpha.66
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/avatar.cjs +1 -1
- package/dist/agent/avatar.modern.js +1 -1
- package/dist/agent/avatar.module.js +1 -1
- package/dist/agent/avatar.umd.js +1 -1
- package/dist/agent/index.d.ts +2 -0
- package/dist/control/ttsa.d.ts +6 -0
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -0
- 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/modules/ResourceManager.d.ts +2 -0
- package/dist/utils/encodeToken.d.ts +7 -0
- package/package.json +1 -1
- package/src/control/ttsa.ts +45 -5
- package/src/index.ts +32 -10
- package/src/modules/ResourceManager.ts +17 -4
- package/src/utils/encodeToken.ts +15 -0
|
@@ -99,6 +99,8 @@ export default class ResourceManager {
|
|
|
99
99
|
options: TOptions;
|
|
100
100
|
sdk: XmovAvatar;
|
|
101
101
|
session_id?: string;
|
|
102
|
+
/** 单次 init 生命周期内的稳定 request_id,由 generateRequestId 生成;session_id 未返回时也能用于 stopSession 兜底销毁 */
|
|
103
|
+
session_request_id?: string;
|
|
102
104
|
mouthShapeLib: any;
|
|
103
105
|
offlineIdle: IOfflineIdle[];
|
|
104
106
|
offlineCache: Map<string, {
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 生成 request_id:用于 startSession / stopSession 的幂等与兜底销毁。
|
|
3
|
+
* 组成:md5(机器指纹 + 毫秒时间戳 + appId)
|
|
4
|
+
* 同一次 init → destroy 生命周期内始终不变,即使 session_id 尚未返回
|
|
5
|
+
* 也能用 request_id 让服务端销毁已占用的房间资源。
|
|
6
|
+
*/
|
|
7
|
+
export declare function generateRequestId(appId: string): string;
|
|
1
8
|
export interface Headers {
|
|
2
9
|
[key: string]: string;
|
|
3
10
|
}
|
package/package.json
CHANGED
package/src/control/ttsa.ts
CHANGED
|
@@ -24,6 +24,7 @@ import '../proto/protobuf.min';
|
|
|
24
24
|
import '../proto/face_data_pb';
|
|
25
25
|
|
|
26
26
|
const proxyProtobuf = protobuf.roots.default;
|
|
27
|
+
const CLIENT_QUIT_ACK_TIMEOUT_MS = 3000;
|
|
27
28
|
|
|
28
29
|
const pointNameObj = {
|
|
29
30
|
'connect_sdk': '连接SDK',
|
|
@@ -63,6 +64,9 @@ interface ITtsaOptions {
|
|
|
63
64
|
export default class Ttsa {
|
|
64
65
|
private TAG = "[TTSA]";
|
|
65
66
|
private ws: Socket;
|
|
67
|
+
private connectedOnce = false;
|
|
68
|
+
private quitRequested = false;
|
|
69
|
+
private quitPromise: Promise<void> | null = null;
|
|
66
70
|
private room: string;
|
|
67
71
|
private session_id: string;
|
|
68
72
|
private token: string;
|
|
@@ -182,6 +186,11 @@ export default class Ttsa {
|
|
|
182
186
|
});
|
|
183
187
|
|
|
184
188
|
ws.on("connect", () => {
|
|
189
|
+
if (this.quitRequested) {
|
|
190
|
+
ws.disconnect();
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
this.connectedOnce = true;
|
|
185
194
|
// 连接成功时清理重连定时器
|
|
186
195
|
this.clearReconnectTimer();
|
|
187
196
|
this.initWsTimeoutTimer();
|
|
@@ -204,6 +213,9 @@ export default class Ttsa {
|
|
|
204
213
|
}
|
|
205
214
|
});
|
|
206
215
|
ws.on("first_start_timestamp", (e: any) => {
|
|
216
|
+
if (this.quitRequested) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
207
219
|
(window as any).avatarSDKLogger.log(this.TAG, "first_start_timestamp", e);
|
|
208
220
|
// 前端在发送first_start_timestamp后运行帧索引
|
|
209
221
|
this.firstStartTimestamp(e.server_time);
|
|
@@ -342,7 +354,7 @@ export default class Ttsa {
|
|
|
342
354
|
this.clearOldWsTimeoutTimer();
|
|
343
355
|
// 停止客户端心跳
|
|
344
356
|
this.stopHeartbeat();
|
|
345
|
-
if(e === 'io client disconnect') {
|
|
357
|
+
if (this.quitRequested || e === 'io client disconnect') {
|
|
346
358
|
return;
|
|
347
359
|
}
|
|
348
360
|
// 增加状态检查,避免重复处理断开事件
|
|
@@ -393,6 +405,10 @@ export default class Ttsa {
|
|
|
393
405
|
}) as any;
|
|
394
406
|
ws.on("client_quit", (e) => {
|
|
395
407
|
(window as any).avatarSDKLogger.warn(this.TAG, "client_quit", "断开原因", e);
|
|
408
|
+
// A quit broadcast may precede its ACK. Let the local quit flow own teardown.
|
|
409
|
+
if (this.quitRequested) {
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
396
412
|
ws.disconnect();
|
|
397
413
|
this.session_speak_req_id = 0;
|
|
398
414
|
if (Object.keys(e).length) {
|
|
@@ -442,6 +458,9 @@ export default class Ttsa {
|
|
|
442
458
|
};
|
|
443
459
|
initWsTimeoutTimer = () => {
|
|
444
460
|
this.clearOldWsTimeoutTimer();
|
|
461
|
+
if (this.quitRequested) {
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
445
464
|
|
|
446
465
|
this.wsConnectTimer = window.setTimeout(async () => {
|
|
447
466
|
|
|
@@ -658,6 +677,10 @@ export default class Ttsa {
|
|
|
658
677
|
return this.ws.connected;
|
|
659
678
|
}
|
|
660
679
|
|
|
680
|
+
get hasConnected(): boolean {
|
|
681
|
+
return this.connectedOnce;
|
|
682
|
+
}
|
|
683
|
+
|
|
661
684
|
_setResumeInfoCallback(lastSessionId: string, _getResumeInfo: () => IResumeParams) {
|
|
662
685
|
this._isResume = true
|
|
663
686
|
this._lastSessionId = lastSessionId
|
|
@@ -682,6 +705,26 @@ export default class Ttsa {
|
|
|
682
705
|
return `${this._uniqueSpeakId}-${this.session_id}`
|
|
683
706
|
}
|
|
684
707
|
|
|
708
|
+
quit(stopReason: string, timeoutMs = CLIENT_QUIT_ACK_TIMEOUT_MS): Promise<void> {
|
|
709
|
+
if (this.quitPromise) {
|
|
710
|
+
return this.quitPromise;
|
|
711
|
+
}
|
|
712
|
+
this.quitRequested = true;
|
|
713
|
+
this.clearReconnectTimer();
|
|
714
|
+
this.clearOldWsTimeoutTimer();
|
|
715
|
+
this.stopHeartbeat();
|
|
716
|
+
this.quitPromise = this.sendClientQuit(stopReason, timeoutMs);
|
|
717
|
+
return this.quitPromise;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
private async sendClientQuit(stopReason: string, timeoutMs: number): Promise<void> {
|
|
721
|
+
if (!this.ws.connected) {
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
// ACK confirms receipt; no application-level ACK payload is assumed here.
|
|
725
|
+
await this.ws.timeout(timeoutMs).emitWithAck("client_quit", { stop_reason: stopReason });
|
|
726
|
+
}
|
|
727
|
+
|
|
685
728
|
close() {
|
|
686
729
|
// 使用 disconnect() 而不是 close(),disconnect() 会停止所有重连尝试
|
|
687
730
|
if (this.ws) {
|
|
@@ -690,10 +733,7 @@ export default class Ttsa {
|
|
|
690
733
|
this.clearOldWsTimeoutTimer();
|
|
691
734
|
this.stopHeartbeat();
|
|
692
735
|
this.session_speak_req_id = 0;
|
|
693
|
-
|
|
694
|
-
clearTimeout(this.reconnectTimer);
|
|
695
|
-
this.reconnectTimer = -1;
|
|
696
|
-
}
|
|
736
|
+
this.clearReconnectTimer();
|
|
697
737
|
if (this.syncTimestampInterval) {
|
|
698
738
|
clearInterval(this.syncTimestampInterval);
|
|
699
739
|
this.syncTimestampInterval = 0; // 重置为初始值 0,保持状态统一
|
package/src/index.ts
CHANGED
|
@@ -1424,9 +1424,7 @@ export default class XmovAvatar {
|
|
|
1424
1424
|
|
|
1425
1425
|
// 薄客户端模式:通知 server 端销毁,然后清理本地资源
|
|
1426
1426
|
if (this._isClientMode) {
|
|
1427
|
-
this.
|
|
1428
|
-
await this.destroyClient();
|
|
1429
|
-
this.onStatusChange(AvatarStatus.close);
|
|
1427
|
+
await this.disconnectClientForDestroy(stop_reason);
|
|
1430
1428
|
return;
|
|
1431
1429
|
}
|
|
1432
1430
|
|
|
@@ -1441,6 +1439,30 @@ export default class XmovAvatar {
|
|
|
1441
1439
|
return res
|
|
1442
1440
|
}
|
|
1443
1441
|
|
|
1442
|
+
protected async disconnectClientForDestroy(stopReason: string): Promise<void> {
|
|
1443
|
+
this.replayData = null;
|
|
1444
|
+
try {
|
|
1445
|
+
if (this._isClientMode) {
|
|
1446
|
+
this._apiForwarder?.destroy(stopReason);
|
|
1447
|
+
} else if (!this.ttsa?.hasConnected) {
|
|
1448
|
+
// Initialization can own a session/request before TTSA first connects.
|
|
1449
|
+
// Mark an in-flight connection as quitting before waiting for DELETE.
|
|
1450
|
+
await this.ttsa?.quit(stopReason);
|
|
1451
|
+
await this.resourceManager.stopSession(stopReason);
|
|
1452
|
+
} else {
|
|
1453
|
+
this.ttsa.sendSdkPoint('close_session', {
|
|
1454
|
+
reason: stopReason,
|
|
1455
|
+
});
|
|
1456
|
+
await this.ttsa.quit(stopReason);
|
|
1457
|
+
}
|
|
1458
|
+
} catch (error) {
|
|
1459
|
+
(window as any).avatarSDKLogger.warn(this.TAG, "TTSA client_quit 未确认,继续释放本地资源", error);
|
|
1460
|
+
} finally {
|
|
1461
|
+
await this.destroyClient();
|
|
1462
|
+
this.onStatusChange(AvatarStatus.close);
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1444
1466
|
idle() {
|
|
1445
1467
|
if (this._isClientMode) { this._apiForwarder?.idle(); return; }
|
|
1446
1468
|
if(this.enableClientInterrupt) {
|
|
@@ -1629,13 +1651,13 @@ export default class XmovAvatar {
|
|
|
1629
1651
|
const e = Errors(params)
|
|
1630
1652
|
if(params.code !== EErrorCode.RENDER_BODY_ERROR && params.code !== EErrorCode.RENDER_FACE_ERROR && params.code !== EErrorCode.BODY_DATA_EXPIRED) {
|
|
1631
1653
|
this.options.onMessage(e)
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1654
|
+
// Sentry 错误上报(仅公有云构建时生效)
|
|
1655
|
+
if (ENABLE_SENTRY && this.sentryReporter) {
|
|
1656
|
+
try {
|
|
1657
|
+
this.sentryReporter.captureError(e);
|
|
1658
|
+
} catch (err) {
|
|
1659
|
+
(window as any).avatarSDKLogger.warn(this.TAG, 'Sentry 上报失败', err);
|
|
1660
|
+
}
|
|
1639
1661
|
}
|
|
1640
1662
|
}
|
|
1641
1663
|
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
} from "../utils/DataInterface";
|
|
7
7
|
import XmovAvatar from "../index";
|
|
8
8
|
import { EErrorCode } from "../types/error";
|
|
9
|
-
import { headersNeedSign } from "../utils/encodeToken";
|
|
9
|
+
import { headersNeedSign, generateRequestId } from "../utils/encodeToken";
|
|
10
10
|
// import { isDEV } from "../utils";
|
|
11
11
|
import CacheManager from './cache-manager';
|
|
12
12
|
|
|
@@ -121,6 +121,8 @@ export default class ResourceManager {
|
|
|
121
121
|
options: TOptions;
|
|
122
122
|
sdk: XmovAvatar;
|
|
123
123
|
session_id?: string;
|
|
124
|
+
/** 单次 init 生命周期内的稳定 request_id,由 generateRequestId 生成;session_id 未返回时也能用于 stopSession 兜底销毁 */
|
|
125
|
+
session_request_id?: string;
|
|
124
126
|
mouthShapeLib: any;
|
|
125
127
|
offlineIdle: IOfflineIdle[] = []
|
|
126
128
|
offlineCache = new Map<string, {
|
|
@@ -304,6 +306,7 @@ export default class ResourceManager {
|
|
|
304
306
|
onDownloadProgress?.(this.progress);
|
|
305
307
|
}, 100);
|
|
306
308
|
// http 请求算 10 进度
|
|
309
|
+
this.session_request_id = generateRequestId(this.options.appId);
|
|
307
310
|
const result = await this.startSession();
|
|
308
311
|
const res = result as unknown as ISessionResponse;
|
|
309
312
|
// 先记录 session_id,确保后续 stopSession 能正确释放房间
|
|
@@ -365,6 +368,7 @@ export default class ResourceManager {
|
|
|
365
368
|
|
|
366
369
|
|
|
367
370
|
const data = {
|
|
371
|
+
request_id: this.session_request_id,
|
|
368
372
|
tag: this.options.tag,
|
|
369
373
|
gateway_type: this.options.gateway_type,
|
|
370
374
|
...(this.options.asr_id !== undefined ? { asr_id: this.options.asr_id } : {}),
|
|
@@ -404,12 +408,19 @@ export default class ResourceManager {
|
|
|
404
408
|
async stopSession(stop_reason: string) {
|
|
405
409
|
try {
|
|
406
410
|
const url = `${this.options.gatewayServer}`;
|
|
407
|
-
|
|
411
|
+
// session_id 尚未返回时(如 init 未完成就 destroy),只要有 request_id 仍发请求,
|
|
412
|
+
// 让服务端用 request_id 匹配并销毁房间占用,避免 init 失败后的资源泄漏。
|
|
413
|
+
if (!this.session_id && !this.session_request_id) {
|
|
408
414
|
return;
|
|
409
415
|
}
|
|
410
|
-
const data = {
|
|
411
|
-
session_id: this.session_id,
|
|
416
|
+
const data: Record<string, any> = {
|
|
412
417
|
stop_reason,
|
|
418
|
+
};
|
|
419
|
+
if (this.session_id) {
|
|
420
|
+
data.session_id = this.session_id;
|
|
421
|
+
}
|
|
422
|
+
if (this.session_request_id) {
|
|
423
|
+
data.request_id = this.session_request_id;
|
|
413
424
|
}
|
|
414
425
|
const { headers, data: signedData } = headersNeedSign(this.options.appId, this.options.appSecret, "DELETE", url, data);
|
|
415
426
|
const response = await request(url, { method: "DELETE", data: signedData, headers: { ...headers, ...this.options.headers } });
|
|
@@ -908,5 +919,7 @@ export default class ResourceManager {
|
|
|
908
919
|
this.bg = null;
|
|
909
920
|
}
|
|
910
921
|
this.progress = 0
|
|
922
|
+
this.session_id = undefined;
|
|
923
|
+
this.session_request_id = undefined;
|
|
911
924
|
}
|
|
912
925
|
}
|
package/src/utils/encodeToken.ts
CHANGED
|
@@ -1,9 +1,24 @@
|
|
|
1
1
|
import md5 from 'blueimp-md5';
|
|
2
|
+
import { generateFingerprint } from './index';
|
|
2
3
|
|
|
3
4
|
function encodeWithMd5(s: string): string {
|
|
4
5
|
return md5(s);
|
|
5
6
|
}
|
|
6
7
|
|
|
8
|
+
/**
|
|
9
|
+
* 生成 request_id:用于 startSession / stopSession 的幂等与兜底销毁。
|
|
10
|
+
* 组成:md5(机器指纹 + 毫秒时间戳 + appId)
|
|
11
|
+
* 同一次 init → destroy 生命周期内始终不变,即使 session_id 尚未返回
|
|
12
|
+
* 也能用 request_id 让服务端销毁已占用的房间资源。
|
|
13
|
+
*/
|
|
14
|
+
export function generateRequestId(appId: string): string {
|
|
15
|
+
const fingerprint = (() => {
|
|
16
|
+
try { return generateFingerprint(); } catch { return 'unknown'; }
|
|
17
|
+
})();
|
|
18
|
+
const payload = `${fingerprint}|${Date.now()}|${appId}`;
|
|
19
|
+
return md5(payload);
|
|
20
|
+
}
|
|
21
|
+
|
|
7
22
|
function escapeNonAsciiForPythonJson(value: string): string {
|
|
8
23
|
return value.replace(/[^\u0000-\u007E]/g, character => {
|
|
9
24
|
return `\\u${character.charCodeAt(0).toString(16).padStart(4, '0')}`;
|