@xmov/avatar 2.0.0-alpha.65 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmov/avatar",
3
- "version": "2.0.0-alpha.65",
3
+ "version": "2.0.0-alpha.66",
4
4
  "description": "魔珐数字人 JS-SDK",
5
5
  "type": "module",
6
6
  "source": "src/index.ts",
@@ -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
- if (this.reconnectTimer !== -1) {
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._apiForwarder?.destroy(stop_reason);
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) {