@xmov/avatar 2.0.0-alpha.38

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