@xmov/avatar 2.4.0 → 2.5.0-alpha.1
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 +34 -6
- package/package-dist/agent/avatar.cjs +1 -0
- package/package-dist/agent/avatar.modern.js +1 -0
- package/package-dist/agent/avatar.module.js +1 -0
- package/package-dist/agent/avatar.umd.js +1 -0
- package/package-dist/agent/index.d.ts +42 -0
- package/{dist/agent/types.d.ts → package-dist/agent/public-types.d.ts} +115 -116
- package/package-dist/index.cjs +3 -0
- package/package-dist/index.d.ts +34 -0
- package/package-dist/index.modern.js +1 -0
- package/package-dist/index.module.js +1 -0
- package/package-dist/index.umd.js +14 -0
- package/package-dist/public-types.d.ts +205 -0
- package/package.json +41 -33
- package/dist/agent/avatar.cjs +0 -1
- package/dist/agent/avatar.modern.js +0 -1
- package/dist/agent/avatar.module.js +0 -1
- package/dist/agent/avatar.umd.js +0 -1
- package/dist/agent/index.d.ts +0 -137
- package/dist/baseRender/AudioRenderer.d.ts +0 -46
- package/dist/baseRender/AudioWorklet.d.ts +0 -78
- package/dist/baseRender/AvatarRenderer.d.ts +0 -225
- package/dist/baseRender/MSEAudioPlayer.d.ts +0 -51
- package/dist/baseRender/UIRenderer.d.ts +0 -39
- package/dist/baseRender/pcm-audio-processor.d.ts +0 -63
- package/dist/control/APIForwarder.d.ts +0 -23
- package/dist/control/DataCacheQueue.d.ts +0 -69
- package/dist/control/EventDispatcher.d.ts +0 -35
- package/dist/control/RenderScheduler.d.ts +0 -171
- package/dist/control/SaveAndDownload.d.ts +0 -14
- package/dist/control/StreamingClient.d.ts +0 -33
- package/dist/control/VideoPlayer.d.ts +0 -47
- package/dist/control/ttsa.d.ts +0 -125
- package/dist/encoding/media-recorder-encoder.d.ts +0 -33
- package/dist/encoding/pcm-webm-encoder.d.ts +0 -13
- package/dist/encoding/webcodec-opus-encoder.d.ts +0 -41
- package/dist/encoding/webm-muxer.d.ts +0 -90
- package/dist/index.cjs +0 -2
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.ts +0 -251
- package/dist/index.modern.js +0 -2
- package/dist/index.modern.js.map +0 -1
- package/dist/index.module.js +0 -2
- package/dist/index.module.js.map +0 -1
- package/dist/index.umd.js +0 -2
- package/dist/index.umd.js.map +0 -1
- package/dist/modules/Composition.d.ts +0 -28
- package/dist/modules/ResourceManager.d.ts +0 -299
- package/dist/modules/TrackRenderer/base-track.d.ts +0 -4
- package/dist/modules/TrackRenderer/index.d.ts +0 -12
- package/dist/modules/TrackRenderer/render-implements.d.ts +0 -19
- package/dist/modules/TrackRenderer/track-pic.d.ts +0 -8
- package/dist/modules/TrackRenderer/track-subtitle.d.ts +0 -8
- package/dist/modules/cache-manager.d.ts +0 -5
- package/dist/modules/decoder.d.ts +0 -84
- package/dist/modules/error-handle.d.ts +0 -17
- package/dist/modules/network.d.ts +0 -21
- package/dist/modules/sentry-reporter.d.ts +0 -58
- package/dist/proto/face_data_pb.d.ts +0 -0
- package/dist/types/capability.d.ts +0 -15
- package/dist/types/error.d.ts +0 -70
- package/dist/types/event.d.ts +0 -25
- package/dist/types/frame-data.d.ts +0 -107
- package/dist/types/index.d.ts +0 -196
- package/dist/types/render.d.ts +0 -18
- package/dist/utils/DataInterface.d.ts +0 -113
- package/dist/utils/GLDevice.d.ts +0 -46
- package/dist/utils/GLPipeline.d.ts +0 -84
- package/dist/utils/GLPipelineDebugTools.d.ts +0 -5
- package/dist/utils/Math.d.ts +0 -21
- package/dist/utils/__tests__/capability-checker.test.d.ts +0 -1
- package/dist/utils/audio-session.d.ts +0 -11
- package/dist/utils/audio.d.ts +0 -1
- package/dist/utils/capability-checker.d.ts +0 -28
- package/dist/utils/encodeToken.d.ts +0 -14
- package/dist/utils/face.d.ts +0 -1
- package/dist/utils/float32-decoder.d.ts +0 -26
- package/dist/utils/fpsTracker.d.ts +0 -27
- package/dist/utils/index.d.ts +0 -22
- package/dist/utils/logger.d.ts +0 -9
- package/dist/utils/media-recorder-timestamp.d.ts +0 -15
- package/dist/utils/pcmAnalyzer.d.ts +0 -40
- package/dist/utils/perfermance.d.ts +0 -15
- package/dist/utils/request.d.ts +0 -20
- package/dist/utils/requestAnimateFrames.d.ts +0 -31
- package/dist/utils/time.d.ts +0 -9
- package/dist/utils/video-probe-asset.d.ts +0 -28
- package/dist/view/DebugOverlay.d.ts +0 -54
- package/dist/worker/streaming-video.d.ts +0 -1
- package/src/baseRender/AudioRenderer.ts +0 -328
- package/src/baseRender/AudioWorklet.ts +0 -776
- package/src/baseRender/AvatarRenderer.ts +0 -1550
- package/src/baseRender/MSEAudioPlayer.ts +0 -254
- package/src/baseRender/UIRenderer.ts +0 -195
- package/src/baseRender/pcm-audio-processor.js +0 -267
- package/src/control/APIForwarder.ts +0 -67
- package/src/control/DataCacheQueue.ts +0 -372
- package/src/control/EventDispatcher.ts +0 -106
- package/src/control/RenderScheduler.ts +0 -1000
- package/src/control/SaveAndDownload.js +0 -77
- package/src/control/StreamingClient.ts +0 -138
- package/src/control/VideoPlayer.ts +0 -306
- package/src/control/ttsa.ts +0 -754
- package/src/encoding/media-recorder-encoder.ts +0 -175
- package/src/encoding/pcm-webm-encoder.ts +0 -72
- package/src/encoding/webcodec-opus-encoder.ts +0 -258
- package/src/encoding/webm-muxer.ts +0 -337
- package/src/global.d.ts +0 -112
- package/src/index.ts +0 -2053
- package/src/modules/Composition.ts +0 -79
- package/src/modules/ResourceManager.ts +0 -926
- package/src/modules/TrackRenderer/base-track.ts +0 -7
- package/src/modules/TrackRenderer/index.ts +0 -40
- package/src/modules/TrackRenderer/render-implements.ts +0 -269
- package/src/modules/TrackRenderer/track-pic.ts +0 -19
- package/src/modules/TrackRenderer/track-subtitle.ts +0 -16
- package/src/modules/cache-manager.ts +0 -10
- package/src/modules/decoder.ts +0 -548
- package/src/modules/error-handle.ts +0 -20
- package/src/modules/network.ts +0 -71
- package/src/modules/sentry-reporter.ts +0 -171
- package/src/proto/face_data.proto +0 -45
- package/src/proto/face_data_pb.js +0 -1297
- package/src/proto/protobuf.min.js +0 -8
- package/src/types/capability.ts +0 -24
- package/src/types/error.ts +0 -211
- package/src/types/event.ts +0 -38
- package/src/types/frame-data.ts +0 -121
- package/src/types/index.ts +0 -220
- package/src/types/render.ts +0 -18
- package/src/utils/DataInterface.ts +0 -746
- package/src/utils/GLDevice.ts +0 -286
- package/src/utils/GLPipeline.ts +0 -1293
- package/src/utils/GLPipelineDebugTools.ts +0 -174
- package/src/utils/Math.ts +0 -266
- package/src/utils/__tests__/capability-checker.test.ts +0 -159
- package/src/utils/audio-session.ts +0 -144
- package/src/utils/audio.ts +0 -44
- package/src/utils/blueimp-md5.d.ts +0 -3
- package/src/utils/capability-checker.ts +0 -762
- package/src/utils/encodeToken.ts +0 -115
- package/src/utils/face.ts +0 -29
- package/src/utils/float32-decoder.js +0 -130
- package/src/utils/fpsTracker.ts +0 -100
- package/src/utils/index.ts +0 -182
- package/src/utils/logger.js +0 -36
- package/src/utils/media-recorder-timestamp.ts +0 -79
- package/src/utils/pcmAnalyzer.ts +0 -154
- package/src/utils/perfermance.ts +0 -141
- package/src/utils/request.ts +0 -107
- package/src/utils/requestAnimateFrames.ts +0 -195
- package/src/utils/time.ts +0 -32
- package/src/utils/video-probe-asset.ts +0 -99
- package/src/view/DebugOverlay.ts +0 -409
- package/src/worker/streaming-video.ts +0 -4647
- /package/{dist → package-dist}/agent/index.cjs +0 -0
- /package/{dist → package-dist}/agent/index.umd.js +0 -0
package/src/control/ttsa.ts
DELETED
|
@@ -1,754 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* WebSocket 模块,即 TTSA 服务
|
|
3
|
-
*/
|
|
4
|
-
import { decode } from "@msgpack/msgpack";
|
|
5
|
-
import io, { Socket } from "socket.io-client";
|
|
6
|
-
import {
|
|
7
|
-
EFrameDataType,
|
|
8
|
-
FRAME_SOURCE_SOURCE,
|
|
9
|
-
IRawAudioFrameData,
|
|
10
|
-
IRawBodyFrameData,
|
|
11
|
-
IRawEventFrameData,
|
|
12
|
-
IRawFrameData,
|
|
13
|
-
ITtsFaceFrameData,
|
|
14
|
-
StateChangeInfo,
|
|
15
|
-
} from "../types/frame-data";
|
|
16
|
-
import type XmovAvatar from "../index";
|
|
17
|
-
import { AvatarStatus, DeviceInfo, Layout, WalkConfig } from "../types";
|
|
18
|
-
import { EErrorCode } from "../types/error";
|
|
19
|
-
import { performanceConstant } from "../utils/perfermance";
|
|
20
|
-
import { IResumeParams } from "../modules/ResourceManager";
|
|
21
|
-
import { scaledInt16BytesToFloat32 } from "../utils/float32-decoder";
|
|
22
|
-
import Pako from 'pako';
|
|
23
|
-
import '../proto/protobuf.min';
|
|
24
|
-
import '../proto/face_data_pb';
|
|
25
|
-
|
|
26
|
-
const proxyProtobuf = protobuf.roots.default;
|
|
27
|
-
const CLIENT_QUIT_ACK_TIMEOUT_MS = 3000;
|
|
28
|
-
|
|
29
|
-
const pointNameObj = {
|
|
30
|
-
'connect_sdk': '连接SDK',
|
|
31
|
-
'connect_sdk_success': '连接SDK成功',
|
|
32
|
-
'llm_text_sdk_received': '大模型输出的文本SDK前端收到',
|
|
33
|
-
'rendering_data_received': '客户端收到音频数据时间',
|
|
34
|
-
'rendering_display': '客户端播放音频数据时间',
|
|
35
|
-
'close_session': '关闭会话',
|
|
36
|
-
'audio_data_expired': '音频数据过期',
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
interface ITtsaOptions {
|
|
40
|
-
sdkInstance: XmovAvatar;
|
|
41
|
-
url: string;
|
|
42
|
-
path?: string;
|
|
43
|
-
room: string;
|
|
44
|
-
session_id: string;
|
|
45
|
-
token: string;
|
|
46
|
-
appInfo: {
|
|
47
|
-
appId: string;
|
|
48
|
-
env: string;
|
|
49
|
-
};
|
|
50
|
-
framedata_proto_version: number;
|
|
51
|
-
onReady(): void;
|
|
52
|
-
handleMessage: (type: EFrameDataType, data: IRawFrameData[]) => void;
|
|
53
|
-
handleAAFrame: (data: any) => void;
|
|
54
|
-
runStartFrameIndex: (server_time: number) => void;
|
|
55
|
-
ttsaStateChangeHandle: (state: StateChangeInfo) => void;
|
|
56
|
-
reloadSuccess: () => void;
|
|
57
|
-
enterOfflineMode: () => void;
|
|
58
|
-
reStartSDK: () => void;
|
|
59
|
-
reconnect_client_timeout: number;
|
|
60
|
-
reconnect_timeout: number;
|
|
61
|
-
sendVoiceEnd: () => void;
|
|
62
|
-
getCurrentFrameIndex: () => number;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export default class Ttsa {
|
|
66
|
-
private TAG = "[TTSA]";
|
|
67
|
-
private ws: Socket;
|
|
68
|
-
private connectedOnce = false;
|
|
69
|
-
private quitRequested = false;
|
|
70
|
-
private quitPromise: Promise<void> | null = null;
|
|
71
|
-
private room: string;
|
|
72
|
-
private session_id: string;
|
|
73
|
-
private token: string;
|
|
74
|
-
private framedata_proto_version: number;
|
|
75
|
-
private getResumeInfo?: () => IResumeParams;
|
|
76
|
-
private runStartFrameIndex: (client_time: number) => void;
|
|
77
|
-
private ttsaStateChangeHandle: (state: StateChangeInfo) => void;
|
|
78
|
-
private sdk: XmovAvatar;
|
|
79
|
-
private reloadSuccess: () => void;
|
|
80
|
-
private _lastSessionId = "";
|
|
81
|
-
private _uniqueSpeakId: number;
|
|
82
|
-
private session_speak_req_id: number;
|
|
83
|
-
private enterOfflineMode: () => void;
|
|
84
|
-
private reStartSDK: () => void;
|
|
85
|
-
private reconnect_client_timeout: number;
|
|
86
|
-
private reconnect_timeout: number;
|
|
87
|
-
private reconnectTimer = -1; // 重开房间定时器(reconnect_client_timeout 后 stopSession + reStartSDK)
|
|
88
|
-
private socketReconnectTimer = -1; // 停止 socket.io 自动重连定时器(reconnect_timeout 后旧房间已失效)
|
|
89
|
-
private wsConnectTimer = 0;
|
|
90
|
-
private enterOfflineFlag = false;
|
|
91
|
-
private sendVoiceEnd: () => void;
|
|
92
|
-
private syncTimestampInterval: any = 0;
|
|
93
|
-
// 每 5 秒检查一次
|
|
94
|
-
private syncTimestampNum: number = 5 * 1000;
|
|
95
|
-
// 真正目标:5 分钟
|
|
96
|
-
private readonly SYNC_INTERVAL = 5 * 60 * 1000;
|
|
97
|
-
// 上次同步时间
|
|
98
|
-
private lastSyncTime = Date.now();
|
|
99
|
-
private getCurrentFrameIndex: () => number;
|
|
100
|
-
// 客户端心跳定时器(每 2s 发送一次)
|
|
101
|
-
private heartbeatTimer: number = -1;
|
|
102
|
-
private readonly HEARTBEAT_INTERVAL = 2000;
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* 清除WebSocket重连定时器(含停止 socket.io 自动重连的定时器)
|
|
106
|
-
*/
|
|
107
|
-
public clearReconnectTimer(): void {
|
|
108
|
-
if (this.reconnectTimer !== -1) {
|
|
109
|
-
clearTimeout(this.reconnectTimer);
|
|
110
|
-
this.reconnectTimer = -1;
|
|
111
|
-
}
|
|
112
|
-
// 一并清理停止 socket.io 自动重连的定时器:网络恢复后交由 socket.io 自行重连,
|
|
113
|
-
// 不再强制断开(下次 disconnect 会重新按窗口设置)
|
|
114
|
-
this.clearSocketReconnectTimer();
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* 清除停止 socket.io 自动重连的定时器
|
|
119
|
-
*/
|
|
120
|
-
private clearSocketReconnectTimer(): void {
|
|
121
|
-
if (this.socketReconnectTimer !== -1) {
|
|
122
|
-
clearTimeout(this.socketReconnectTimer);
|
|
123
|
-
this.socketReconnectTimer = -1;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* 启动客户端心跳(每 2s 发送一次 client_heartbeat)
|
|
129
|
-
*/
|
|
130
|
-
private startHeartbeat(): void {
|
|
131
|
-
this.stopHeartbeat();
|
|
132
|
-
this.heartbeatTimer = window.setInterval(() => {
|
|
133
|
-
this.send("client_heartbeat", {
|
|
134
|
-
timestamp: Date.now(),
|
|
135
|
-
frame: this.getCurrentFrameIndex(),
|
|
136
|
-
});
|
|
137
|
-
}, this.HEARTBEAT_INTERVAL);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* 停止客户端心跳
|
|
142
|
-
*/
|
|
143
|
-
private stopHeartbeat(): void {
|
|
144
|
-
if (this.heartbeatTimer !== -1) {
|
|
145
|
-
clearInterval(this.heartbeatTimer);
|
|
146
|
-
this.heartbeatTimer = -1;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
private _isResume = false;
|
|
151
|
-
private appInfo: {
|
|
152
|
-
appId: string;
|
|
153
|
-
env: string;
|
|
154
|
-
};
|
|
155
|
-
constructor(options: ITtsaOptions) {
|
|
156
|
-
this.appInfo = options.appInfo;
|
|
157
|
-
this.sdk = options.sdkInstance;
|
|
158
|
-
this.room = options.room;
|
|
159
|
-
this.session_id = options.session_id;
|
|
160
|
-
this.token = options.token;
|
|
161
|
-
this.framedata_proto_version = options.framedata_proto_version;
|
|
162
|
-
this._uniqueSpeakId = 0;
|
|
163
|
-
this.runStartFrameIndex = options.runStartFrameIndex;
|
|
164
|
-
this.ttsaStateChangeHandle = options.ttsaStateChangeHandle;
|
|
165
|
-
this.reloadSuccess = options.reloadSuccess;
|
|
166
|
-
this.session_speak_req_id = 0;
|
|
167
|
-
this.enterOfflineMode = options.enterOfflineMode;
|
|
168
|
-
this.reStartSDK = options.reStartSDK;
|
|
169
|
-
this.reconnect_client_timeout = options.reconnect_client_timeout;
|
|
170
|
-
this.reconnect_timeout = options.reconnect_timeout;
|
|
171
|
-
this.sendVoiceEnd = options.sendVoiceEnd;
|
|
172
|
-
this.getCurrentFrameIndex = options.getCurrentFrameIndex;
|
|
173
|
-
|
|
174
|
-
const ws = io(options.url, {
|
|
175
|
-
query: {
|
|
176
|
-
token: this.token,
|
|
177
|
-
},
|
|
178
|
-
path: options.path || "",
|
|
179
|
-
transports: ['websocket'],
|
|
180
|
-
// 自动重连不限制次数:在 reconnect_timeout(服务端断开旧房间的窗口)内持续尝试,
|
|
181
|
-
// 失效房间必然重连失败;超过窗口由 disconnect 处理器中的停止定时器断开,
|
|
182
|
-
// 超过 reconnect_client_timeout 由重开房间定时器兜底(stopSession + reStartSDK)
|
|
183
|
-
reconnection: true,
|
|
184
|
-
reconnectionAttempts: Infinity,
|
|
185
|
-
reconnectionDelay: 1000,
|
|
186
|
-
reconnectionDelayMax: 1000,
|
|
187
|
-
randomizationFactor: 0.3
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
ws.on("connect", () => {
|
|
191
|
-
if (this.quitRequested) {
|
|
192
|
-
ws.disconnect();
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
this.connectedOnce = true;
|
|
196
|
-
// 连接成功时清理重连定时器
|
|
197
|
-
this.clearReconnectTimer();
|
|
198
|
-
this.initWsTimeoutTimer();
|
|
199
|
-
// 启动客户端心跳
|
|
200
|
-
this.startHeartbeat();
|
|
201
|
-
// 重置离线处理标志:重连成功后允许下次断网正常进入离线/重连流程
|
|
202
|
-
this.enterOfflineFlag = false;
|
|
203
|
-
(window as any).performanceTracker.markEnd(performanceConstant.ttsa_connect);
|
|
204
|
-
const payload = {
|
|
205
|
-
room: this.room,
|
|
206
|
-
client_type: "web",
|
|
207
|
-
invisible_mode: this.sdk.getPendingInvisibleMode(),
|
|
208
|
-
};
|
|
209
|
-
this.send("enter_room", payload);
|
|
210
|
-
// 窗口内自动重连恢复:服务端可能不重发 first_start_timestamp,
|
|
211
|
-
// 若仅依赖 reloadSuccess 补发 online/恢复播放,状态会卡在离线导致重连后卡死。
|
|
212
|
-
// 因此重连成功即恢复(仅初始化完成且此前已离线时;初次启动 status 为 -1 不受影响)
|
|
213
|
-
if (this.sdk.getStatus() === AvatarStatus.offline && this.sdk.getIsInitialized()) {
|
|
214
|
-
this.reloadSuccess();
|
|
215
|
-
}
|
|
216
|
-
});
|
|
217
|
-
ws.on("first_start_timestamp", (e: any) => {
|
|
218
|
-
if (this.quitRequested) {
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
(window as any).avatarSDKLogger.log(this.TAG, "first_start_timestamp", e);
|
|
222
|
-
// 前端在发送first_start_timestamp后运行帧索引
|
|
223
|
-
this.firstStartTimestamp(e.server_time);
|
|
224
|
-
(window as any).performanceTracker.markEnd(performanceConstant.ttsa_ready);
|
|
225
|
-
(window as any).performanceTracker.markStart(performanceConstant.ttsa_body_res);
|
|
226
|
-
(window as any).avatarSDKLogger.log(this.TAG, 'ready');
|
|
227
|
-
this.reloadSuccess()
|
|
228
|
-
options.onReady();
|
|
229
|
-
});
|
|
230
|
-
ws.on("state_change", (e: StateChangeInfo) => {
|
|
231
|
-
this.ttsaStateChangeHandle(e);
|
|
232
|
-
});
|
|
233
|
-
ws.on("tts_audio", (e: any) => {
|
|
234
|
-
try {
|
|
235
|
-
const decodedData = decode(e) as IRawAudioFrameData[];
|
|
236
|
-
this.sendSdkPoint('rendering_data_received', { multi_turn_conversation_id: decodedData[0].multi_turn_conversation_id });
|
|
237
|
-
|
|
238
|
-
(window as any).avatarSDKLogger.log(this.TAG, "下发音频数据tts_audio", decodedData);
|
|
239
|
-
options.handleMessage(EFrameDataType.AUDIO, decodedData);
|
|
240
|
-
} catch (error) {
|
|
241
|
-
this.sdk.onMessage({
|
|
242
|
-
code: EErrorCode.AUDIO_DECODE_ERROR,
|
|
243
|
-
message: `Error: 音频数据解码失败`,
|
|
244
|
-
e: error,
|
|
245
|
-
});
|
|
246
|
-
}
|
|
247
|
-
});
|
|
248
|
-
ws.on("face_data", async (e: any) => {
|
|
249
|
-
try {
|
|
250
|
-
if(!this.framedata_proto_version) {
|
|
251
|
-
const faceDecodeData = decode(e) as ITtsFaceFrameData[];
|
|
252
|
-
(window as any).avatarSDKLogger.log(this.TAG, "face_data", faceDecodeData);
|
|
253
|
-
if(faceDecodeData[0].s !== 'speak' && this.enterOfflineFlag) {
|
|
254
|
-
// 发送voice_end
|
|
255
|
-
this.sendVoiceEnd();
|
|
256
|
-
this.enterOfflineFlag = false;
|
|
257
|
-
}
|
|
258
|
-
options.handleMessage(EFrameDataType.FACE, faceDecodeData);
|
|
259
|
-
} else {
|
|
260
|
-
let frameList: any[] = []
|
|
261
|
-
const uint8Array = new Uint8Array(e);
|
|
262
|
-
const decompressed = Pako.inflate(uint8Array);
|
|
263
|
-
const FaceFrameDataList = proxyProtobuf.FaceFrameDataList.decode(decompressed, undefined, () => { });
|
|
264
|
-
const faceFrameList = FaceFrameDataList.toJSON() || { data: [] };
|
|
265
|
-
frameList = faceFrameList.data.map((frame) => {
|
|
266
|
-
// 1. 解析 JointData 实例(转为普通对象 + 解码 float16)
|
|
267
|
-
const joints = (frame.js || []).map((joint) => {
|
|
268
|
-
// joint 是 protobuf 生成的 JointData 实例,直接访问字段
|
|
269
|
-
return {
|
|
270
|
-
translate: joint.translate || [], // float32 无需解码,直接提取
|
|
271
|
-
rotate: joint.rotate ? scaledInt16BytesToFloat32(joint.rotate) : []
|
|
272
|
-
};
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
// 2. 解析 MeshData 实例(转为普通对象 + 解码 float16)
|
|
276
|
-
const meshes = (frame.ms || []).map((mesh) => {
|
|
277
|
-
return {
|
|
278
|
-
index: mesh.index || 0,
|
|
279
|
-
weights: mesh.weights || []
|
|
280
|
-
};
|
|
281
|
-
});
|
|
282
|
-
return {
|
|
283
|
-
...frame,
|
|
284
|
-
bsw: frame.bsw ? scaledInt16BytesToFloat32(frame.bsw) : [],
|
|
285
|
-
js: joints,
|
|
286
|
-
ms: meshes,
|
|
287
|
-
body_id: frame.bodyId || 0, // proto 中的 body_id → 编译后 bodyId(驼峰)
|
|
288
|
-
face_frame_type: frame.faceFrameType || 0
|
|
289
|
-
};
|
|
290
|
-
})
|
|
291
|
-
options.handleMessage(EFrameDataType.FACE, frameList);
|
|
292
|
-
if(frameList[0].s !== 'speak' && this.enterOfflineFlag) {
|
|
293
|
-
// 发送voice_end
|
|
294
|
-
this.sendVoiceEnd();
|
|
295
|
-
this.enterOfflineFlag = false;
|
|
296
|
-
}
|
|
297
|
-
// const faceDecodeData = decode(e) as ITtsFaceFrameData[];
|
|
298
|
-
(window as any).avatarSDKLogger.log(this.TAG, "face_data", frameList);
|
|
299
|
-
}
|
|
300
|
-
} catch (error) {
|
|
301
|
-
this.sdk.destroy();
|
|
302
|
-
this.sdk.onMessage({
|
|
303
|
-
code: EErrorCode.FACE_DECODE_ERROR,
|
|
304
|
-
message: `Error: 表情数据解码失败`,
|
|
305
|
-
e: error,
|
|
306
|
-
});
|
|
307
|
-
}
|
|
308
|
-
});
|
|
309
|
-
ws.on("body_data", (e: any) => {
|
|
310
|
-
try {
|
|
311
|
-
// 清空离线mode
|
|
312
|
-
let bodyDecodeData = decode(e) as IRawBodyFrameData[];
|
|
313
|
-
(window as any).performanceTracker.markEnd(performanceConstant.start_action_res, bodyDecodeData[0].s);
|
|
314
|
-
const cBody = bodyDecodeData.map(item => ({
|
|
315
|
-
...item,
|
|
316
|
-
x_offset: []
|
|
317
|
-
}));
|
|
318
|
-
(window as any).avatarSDKLogger.log(this.TAG, "body_data", JSON.stringify(cBody), new Date().getTime());
|
|
319
|
-
(window as any).performanceTracker.markEnd(performanceConstant.ttsa_body_res);
|
|
320
|
-
options.handleMessage(EFrameDataType.BODY, bodyDecodeData);
|
|
321
|
-
} catch (error) {
|
|
322
|
-
this.sdk.onMessage({
|
|
323
|
-
code: EErrorCode.VIDEO_DECODE_ERROR,
|
|
324
|
-
message: `Error: 身体数据解码失败`,
|
|
325
|
-
e: error,
|
|
326
|
-
});
|
|
327
|
-
}
|
|
328
|
-
});
|
|
329
|
-
ws.on("event_data", (e: any) => {
|
|
330
|
-
try {
|
|
331
|
-
const decodeData = decode(e) as IRawEventFrameData[];
|
|
332
|
-
(window as any).avatarSDKLogger.log(this.TAG, "event_data", JSON.stringify(decodeData));
|
|
333
|
-
options.handleMessage(EFrameDataType.EVENT, decodeData);
|
|
334
|
-
} catch (error) {
|
|
335
|
-
this.sdk.onMessage({
|
|
336
|
-
code: EErrorCode.EVENT_DECODE_ERROR,
|
|
337
|
-
message: `Error: 事件数据解码失败`,
|
|
338
|
-
e: error
|
|
339
|
-
});
|
|
340
|
-
}
|
|
341
|
-
});
|
|
342
|
-
ws.on("sync_frame", (e: any) => {
|
|
343
|
-
options.handleMessage(EFrameDataType.SYNC_FRAME, e);
|
|
344
|
-
});
|
|
345
|
-
ws.on("aa_frame", (e: any) => {
|
|
346
|
-
(window as any).avatarSDKLogger.log(this.TAG, "下发机器人视频帧aa_frame", e);
|
|
347
|
-
options.handleAAFrame(e);
|
|
348
|
-
});
|
|
349
|
-
ws.on("error_message", (e: any) => {
|
|
350
|
-
this.sdk.onMessage({
|
|
351
|
-
code: e?.error_code || EErrorCode.TTSA_ERROR,
|
|
352
|
-
message: e?.user_hint || 'TTSA 返回异常',
|
|
353
|
-
});
|
|
354
|
-
});
|
|
355
|
-
ws.on("disconnect", (e) => {
|
|
356
|
-
this.clearOldWsTimeoutTimer();
|
|
357
|
-
// 停止客户端心跳
|
|
358
|
-
this.stopHeartbeat();
|
|
359
|
-
if (this.quitRequested || e === 'io client disconnect') {
|
|
360
|
-
return;
|
|
361
|
-
}
|
|
362
|
-
// 增加状态检查,避免重复处理断开事件
|
|
363
|
-
if (this.enterOfflineFlag) {
|
|
364
|
-
(window as any).avatarSDKLogger.warn(this.TAG, "已在离线处理中,忽略重复的disconnect事件");
|
|
365
|
-
return;
|
|
366
|
-
}
|
|
367
|
-
this.sdk.onMessage({
|
|
368
|
-
code: EErrorCode.SOCKET_DISCONNECT,
|
|
369
|
-
message: `client Warning: socket disconnect, ${e}`,
|
|
370
|
-
});
|
|
371
|
-
(window as any).avatarSDKLogger.warn(this.TAG, `${options.url} 已断开`, "disconnect原因", e);
|
|
372
|
-
this.enterOfflineFlag = true;
|
|
373
|
-
if(this.sdk.getStatus() === AvatarStatus.offline) {
|
|
374
|
-
// 已进入离线模式,不销毁
|
|
375
|
-
this.sdk.onStatusChange(AvatarStatus.offline);
|
|
376
|
-
} else {
|
|
377
|
-
// 无网断开则进入离线模式
|
|
378
|
-
this.enterOfflineMode();
|
|
379
|
-
}
|
|
380
|
-
// 重连窗口策略(不再依赖 navigator.onLine,socket 断开即启动):
|
|
381
|
-
// ① socketReconnectTimer(reconnect_timeout):窗口内 socket.io 自动重连;
|
|
382
|
-
// 服务端会在 reconnect_timeout 后断开旧房间,因此窗口结束即停止无谓的自动重连
|
|
383
|
-
// ② reconnectTimer(reconnect_client_timeout):仍未能重连则停止旧连接并重开房间
|
|
384
|
-
// 均仅首次设置,避免 socket.io 重连失败反复触发 disconnect 重置定时器导致永不执行;
|
|
385
|
-
// 重连成功时 connect 处理器、网络恢复时 NetworkMonitor -> clearReconnectTimer 都会取消
|
|
386
|
-
if (this.socketReconnectTimer === -1 && this.reconnect_timeout > 0) {
|
|
387
|
-
this.socketReconnectTimer = window.setTimeout(() => {
|
|
388
|
-
try {
|
|
389
|
-
ws.disconnect();
|
|
390
|
-
} catch (error) {
|
|
391
|
-
(window as any).avatarSDKLogger.warn(this.TAG, "disconnect error", error);
|
|
392
|
-
}
|
|
393
|
-
this.socketReconnectTimer = -1;
|
|
394
|
-
}, this.reconnect_timeout * 1000);
|
|
395
|
-
}
|
|
396
|
-
if (this.reconnectTimer === -1) {
|
|
397
|
-
this.reconnectTimer = window.setTimeout(async () => {
|
|
398
|
-
try {
|
|
399
|
-
ws.disconnect();
|
|
400
|
-
} catch (error) {
|
|
401
|
-
(window as any).avatarSDKLogger.warn(this.TAG, "disconnect error", error);
|
|
402
|
-
}
|
|
403
|
-
await this.sdk.stopSessionFromSocket("WS_TIMEOUT");
|
|
404
|
-
this.reStartSDK();
|
|
405
|
-
}, this.reconnect_client_timeout * 1000);
|
|
406
|
-
}
|
|
407
|
-
}) as any;
|
|
408
|
-
ws.on("client_quit", (e) => {
|
|
409
|
-
(window as any).avatarSDKLogger.warn(this.TAG, "client_quit", "断开原因", e);
|
|
410
|
-
// A quit broadcast may precede its ACK. Let the local quit flow own teardown.
|
|
411
|
-
if (this.quitRequested) {
|
|
412
|
-
return;
|
|
413
|
-
}
|
|
414
|
-
ws.disconnect();
|
|
415
|
-
this.session_speak_req_id = 0;
|
|
416
|
-
if (Object.keys(e).length) {
|
|
417
|
-
this.sdk.onMessage({
|
|
418
|
-
code: EErrorCode.SOCKET_QUIT,
|
|
419
|
-
message: `ttsa主动关闭 reason: ${JSON.stringify({ e })}`,
|
|
420
|
-
e
|
|
421
|
-
})
|
|
422
|
-
}
|
|
423
|
-
// 字符库加载失败,进入离线模式
|
|
424
|
-
// 优化代码
|
|
425
|
-
const not_reStart_reason = ['user', 'char_bin_load_error', 'admin kick', 'nebula admin stop', 'no_time'];
|
|
426
|
-
if(!not_reStart_reason.includes(e?.stop_reason)) {
|
|
427
|
-
if(this.sdk.getStatus() === AvatarStatus.offline) {
|
|
428
|
-
// 已进入离线模式,不销毁
|
|
429
|
-
this.sdk.onStatusChange(AvatarStatus.offline);
|
|
430
|
-
} else {
|
|
431
|
-
// 无网断开则进入离线模式
|
|
432
|
-
this.enterOfflineMode();
|
|
433
|
-
}
|
|
434
|
-
this.reStartSDK();
|
|
435
|
-
} else {
|
|
436
|
-
this.sdk.destroyClient();
|
|
437
|
-
this.sdk.onStatusChange(AvatarStatus.close);
|
|
438
|
-
}
|
|
439
|
-
});
|
|
440
|
-
ws.on("connect_error", (e: any) => {
|
|
441
|
-
this.sdk.onMessage({
|
|
442
|
-
code: EErrorCode.CONNECT_SOCKET_ERROR,
|
|
443
|
-
message: `Error: socket连接失败`,
|
|
444
|
-
e,
|
|
445
|
-
});
|
|
446
|
-
});
|
|
447
|
-
ws.onAny((event, args) => {
|
|
448
|
-
if (this.ws.connected) {
|
|
449
|
-
this.initWsTimeoutTimer();
|
|
450
|
-
}
|
|
451
|
-
})
|
|
452
|
-
this.ws = ws;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
clearOldWsTimeoutTimer = () => {
|
|
456
|
-
if (this.wsConnectTimer) {
|
|
457
|
-
clearTimeout(this.wsConnectTimer);
|
|
458
|
-
this.wsConnectTimer = 0; // 重置为初始值 0,保持状态统一
|
|
459
|
-
}
|
|
460
|
-
};
|
|
461
|
-
initWsTimeoutTimer = () => {
|
|
462
|
-
this.clearOldWsTimeoutTimer();
|
|
463
|
-
if (this.quitRequested) {
|
|
464
|
-
return;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
this.wsConnectTimer = window.setTimeout(async () => {
|
|
468
|
-
|
|
469
|
-
try {
|
|
470
|
-
this.sdk.onMessage({
|
|
471
|
-
code: EErrorCode.CONNECT_SOCKET_ERROR,
|
|
472
|
-
message: `Error: socket长时间未下发数据`,
|
|
473
|
-
});
|
|
474
|
-
|
|
475
|
-
if (this.session_id) {
|
|
476
|
-
await this.sdk.stopSessionFromSocket("WS_NO_DATA_TIMEOUT");
|
|
477
|
-
this.ws.disconnect();
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
this.reStartSDK();
|
|
481
|
-
} finally {
|
|
482
|
-
this.clearOldWsTimeoutTimer();
|
|
483
|
-
}
|
|
484
|
-
}, (this.reconnect_client_timeout + 10) * 1000);
|
|
485
|
-
};
|
|
486
|
-
|
|
487
|
-
start() {
|
|
488
|
-
if (this._isResume) {
|
|
489
|
-
this._isResume = false;
|
|
490
|
-
return;
|
|
491
|
-
}
|
|
492
|
-
this.stateChange("interactive_idle");
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
idle() {
|
|
496
|
-
this.stateChange("idle");
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
// faceDetect() {
|
|
500
|
-
// this.stateChange("face_detect");
|
|
501
|
-
// }
|
|
502
|
-
|
|
503
|
-
// wakeUp() {
|
|
504
|
-
// this.stateChange("wake_up");
|
|
505
|
-
// }
|
|
506
|
-
|
|
507
|
-
listen() {
|
|
508
|
-
this.stateChange("listen");
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
think() {
|
|
512
|
-
this.stateChange("think");
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
// skill(action_semantic: string) {
|
|
516
|
-
// this.stateChange("skill",{
|
|
517
|
-
// action_semantic
|
|
518
|
-
// });
|
|
519
|
-
// }
|
|
520
|
-
|
|
521
|
-
// exitInteraction() {
|
|
522
|
-
// this.stateChange("exit_interaction");
|
|
523
|
-
// }
|
|
524
|
-
|
|
525
|
-
// touchReact() {
|
|
526
|
-
// this.stateChange("touch_react");
|
|
527
|
-
// }
|
|
528
|
-
|
|
529
|
-
interactiveidle() {
|
|
530
|
-
this.stateChange("interactive_idle");
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
/**
|
|
534
|
-
* 通知后端进入隐身模式
|
|
535
|
-
*/
|
|
536
|
-
enterInvisibleMode(): void {
|
|
537
|
-
this.send('switch_invisible_mode', { session_id: this.session_id, invisible_mode: true });
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
/**
|
|
541
|
-
* 通知后端退出隐身模式
|
|
542
|
-
*/
|
|
543
|
-
exitInvisibleMode(): void {
|
|
544
|
-
this.send('switch_invisible_mode', { session_id: this.session_id, invisible_mode: false });
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
firstStartTimestamp(server_time: number) {
|
|
548
|
-
const client_time = Date.now() / 1000;
|
|
549
|
-
const payload = {
|
|
550
|
-
server_time,
|
|
551
|
-
client_time,
|
|
552
|
-
};
|
|
553
|
-
const resumeInfo = this._getResumeInfo()
|
|
554
|
-
if (this._isResume && resumeInfo && resumeInfo.client_frame > 0) {
|
|
555
|
-
payload['resume_from_offline_idle'] = resumeInfo;
|
|
556
|
-
} else {
|
|
557
|
-
// 数据异常,重置_isResume
|
|
558
|
-
this._isResume = false;
|
|
559
|
-
}
|
|
560
|
-
this.runStartFrameIndex(client_time);
|
|
561
|
-
this.send("first_start_timestamp", payload);
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
syncTimestamp() {
|
|
565
|
-
if (this.syncTimestampInterval) {
|
|
566
|
-
clearInterval(this.syncTimestampInterval);
|
|
567
|
-
this.syncTimestampInterval = 0;
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
this.syncTimestampInterval = setInterval(() => {
|
|
571
|
-
const now = Date.now();
|
|
572
|
-
if (now - this.lastSyncTime >= this.SYNC_INTERVAL) {
|
|
573
|
-
const client_frame = this.getCurrentFrameIndex();
|
|
574
|
-
this.sendSyncTimestamp(client_frame);
|
|
575
|
-
this.lastSyncTime = now;
|
|
576
|
-
}
|
|
577
|
-
}, this.syncTimestampNum);
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
sendSyncTimestamp(client_frame: number) {
|
|
581
|
-
const client_time = Date.now() / 1000;
|
|
582
|
-
this.send("sync_frame", { client_time, source: FRAME_SOURCE_SOURCE, client_frame });
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
sendText(ssml: string, is_start: boolean, is_end: boolean, extra = {client_speak_id: ''}) {
|
|
586
|
-
if (!this.ws.connected) {
|
|
587
|
-
return
|
|
588
|
-
}
|
|
589
|
-
const uniqueSpeakId = this.getUniqueSpeakId();
|
|
590
|
-
if(!extra?.client_speak_id) {
|
|
591
|
-
extra.client_speak_id = uniqueSpeakId;
|
|
592
|
-
}
|
|
593
|
-
const payload = {
|
|
594
|
-
ssml,
|
|
595
|
-
is_start,
|
|
596
|
-
is_end,
|
|
597
|
-
extra,
|
|
598
|
-
multi_turn_conversation_id: uniqueSpeakId,
|
|
599
|
-
session_speak_req_id: this.session_speak_req_id,
|
|
600
|
-
timestamp: Date.now(),
|
|
601
|
-
// @ts-ignore
|
|
602
|
-
sdkVersion: VERSION,
|
|
603
|
-
};
|
|
604
|
-
this.sendSdkPoint('llm_text_sdk_received', {
|
|
605
|
-
...extra,
|
|
606
|
-
is_start,
|
|
607
|
-
is_end,
|
|
608
|
-
content: { ssml },
|
|
609
|
-
multi_turn_conversation_id: uniqueSpeakId,
|
|
610
|
-
speak_id: this.session_speak_req_id++,
|
|
611
|
-
});
|
|
612
|
-
if (is_start) {
|
|
613
|
-
(window as any).performanceTracker.markStart(performanceConstant.start_action_res, 'speak');
|
|
614
|
-
(window as any).performanceTracker.markStart(performanceConstant.start_action_render, 'speak');
|
|
615
|
-
(window as any).performanceTracker.markStart(performanceConstant.voice_response_play, 'speak');
|
|
616
|
-
}
|
|
617
|
-
if (is_end) {
|
|
618
|
-
this.updateUniqueSpeakId();
|
|
619
|
-
}
|
|
620
|
-
this.send("send_text", payload);
|
|
621
|
-
return uniqueSpeakId;
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
sendSdkPoint(name: string, params: any = {}, extra?: any) {
|
|
625
|
-
this.send("sdk_burial_point", {
|
|
626
|
-
...params,
|
|
627
|
-
...extra,
|
|
628
|
-
...this.appInfo,
|
|
629
|
-
burial_type: 1,
|
|
630
|
-
session_id: this.session_id,
|
|
631
|
-
event_en_name: name,
|
|
632
|
-
event_cn_name: pointNameObj[name],
|
|
633
|
-
device: navigator.userAgent,
|
|
634
|
-
timestamp: params.timestamp || Date.now(),
|
|
635
|
-
// @ts-ignore
|
|
636
|
-
sdkVersion: VERSION,
|
|
637
|
-
|
|
638
|
-
});
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
stateChange(state: string, params?: object) {
|
|
642
|
-
const payload = {
|
|
643
|
-
state,
|
|
644
|
-
params: params || {},
|
|
645
|
-
};
|
|
646
|
-
(window as any).performanceTracker.markStart(performanceConstant.start_action_res, state);
|
|
647
|
-
(window as any).performanceTracker.markStart(performanceConstant.start_action_render, state);
|
|
648
|
-
this.send("state_change", payload);
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
sendPerfLog(payload: object) {
|
|
652
|
-
// 目前该点位服务端未处理,暂不上报
|
|
653
|
-
// this.ws.emit('perf_log', {
|
|
654
|
-
// payload,
|
|
655
|
-
// sessionId: this._lastSessionId,
|
|
656
|
-
// });
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
changeLayout(layout: Layout) {
|
|
660
|
-
this.send("change_layout", layout);
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
changeWalkConfig(walkConfig: WalkConfig) {
|
|
664
|
-
this.send("walk_config", walkConfig);
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
public send(event: string, payload: Object) {
|
|
668
|
-
if (!this.ws.connected) {
|
|
669
|
-
return
|
|
670
|
-
}
|
|
671
|
-
this.ws.emit(event, payload);
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
sendDeviceInfo(deviceInfo: DeviceInfo) {
|
|
675
|
-
this.send("stat_report", deviceInfo);
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
getStatus() {
|
|
679
|
-
return this.ws.connected;
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
get hasConnected(): boolean {
|
|
683
|
-
return this.connectedOnce;
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
_setResumeInfoCallback(lastSessionId: string, _getResumeInfo: () => IResumeParams) {
|
|
687
|
-
this._isResume = true
|
|
688
|
-
this._lastSessionId = lastSessionId
|
|
689
|
-
this.getResumeInfo = _getResumeInfo
|
|
690
|
-
}
|
|
691
|
-
_getResumeInfo() {
|
|
692
|
-
if (this.getResumeInfo) {
|
|
693
|
-
const resumeInfo = this.getResumeInfo()
|
|
694
|
-
if (this._lastSessionId) {
|
|
695
|
-
resumeInfo['last_session_id'] = this._lastSessionId
|
|
696
|
-
}
|
|
697
|
-
return resumeInfo
|
|
698
|
-
}
|
|
699
|
-
return null
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
updateUniqueSpeakId() {
|
|
703
|
-
this._uniqueSpeakId += 1
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
getUniqueSpeakId() {
|
|
707
|
-
return `${this._uniqueSpeakId}-${this.session_id}`
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
quit(stopReason: string, timeoutMs = CLIENT_QUIT_ACK_TIMEOUT_MS): Promise<void> {
|
|
711
|
-
if (this.quitPromise) {
|
|
712
|
-
return this.quitPromise;
|
|
713
|
-
}
|
|
714
|
-
this.quitRequested = true;
|
|
715
|
-
this.clearReconnectTimer();
|
|
716
|
-
this.clearOldWsTimeoutTimer();
|
|
717
|
-
this.stopHeartbeat();
|
|
718
|
-
this.quitPromise = this.sendClientQuit(stopReason, timeoutMs);
|
|
719
|
-
return this.quitPromise;
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
private async sendClientQuit(stopReason: string, timeoutMs: number): Promise<void> {
|
|
723
|
-
if (!this.ws.connected) {
|
|
724
|
-
return;
|
|
725
|
-
}
|
|
726
|
-
// ACK confirms receipt; no application-level ACK payload is assumed here.
|
|
727
|
-
await this.ws.timeout(timeoutMs).emitWithAck("client_quit", { stop_reason: stopReason });
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
close() {
|
|
731
|
-
// 使用 disconnect() 而不是 close(),disconnect() 会停止所有重连尝试
|
|
732
|
-
if (this.ws) {
|
|
733
|
-
this.ws.disconnect();
|
|
734
|
-
}
|
|
735
|
-
this.clearOldWsTimeoutTimer();
|
|
736
|
-
this.stopHeartbeat();
|
|
737
|
-
this.session_speak_req_id = 0;
|
|
738
|
-
this.clearReconnectTimer();
|
|
739
|
-
if (this.syncTimestampInterval) {
|
|
740
|
-
clearInterval(this.syncTimestampInterval);
|
|
741
|
-
this.syncTimestampInterval = 0; // 重置为初始值 0,保持状态统一
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
connect() {
|
|
746
|
-
this.ws.connect();
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
seekToFrame(frame: number) {
|
|
750
|
-
this.ws.emit('seek_to_frame', {
|
|
751
|
-
frame
|
|
752
|
-
});
|
|
753
|
-
}
|
|
754
|
-
}
|