@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,945 @@
1
+ import { DataCacheQueue } from "./DataCacheQueue";
2
+ import AvatarRenderer from "../baseRender/AvatarRenderer";
3
+ import UIRenderer from "../baseRender/UIRenderer";
4
+ import Composition from "../modules/Composition";
5
+ import ResourceManager from "../modules/ResourceManager";
6
+ import {
7
+ EFrameDataType,
8
+ IRawFrameData,
9
+ IRawBodyFrameData,
10
+ IRawEventFrameData,
11
+ IRawFaceFrameData,
12
+ IRawAudioFrameData,
13
+ ITtsFaceFrameData,
14
+ StateChangeInfo,
15
+ SYNC_FRAME_DATA,
16
+ IRawWidgetData,
17
+ FRAME_SOURCE_SOURCE,
18
+ } from "../types/frame-data";
19
+ import ParallelDecoder from "../modules/decoder";
20
+ import AnimationController from "../utils/requestAnimateFrames";
21
+ import { EErrorCode } from "../types/error";
22
+ import { formatMJT } from "../utils/DataInterface";
23
+ import AudioRenderer from "../baseRender/AudioRenderer";
24
+ import type XmovAvatar from "../index";
25
+ import SaveAndDownload from "./SaveAndDownload";
26
+ import { RenderState, WalkConfig, IFrameDataUpdate } from "../types/index";
27
+ import { Layout } from "../types";
28
+ import WidgetDefaultRender from "../modules/TrackRenderer/render-implements";
29
+ import { performanceConstant } from "../utils/perfermance";
30
+ import { FPSStats } from "../utils/fpsTracker";
31
+
32
+ interface CONFIG {
33
+ sdkInstance: XmovAvatar;
34
+ container: Element;
35
+ hardwareAcceleration: string;
36
+ resourceManager: ResourceManager;
37
+ enableDebugger?: boolean;
38
+ enableClientInterrupt?: boolean;
39
+ audioOnlyMode?: boolean;
40
+ onDownloadProgress: (progress: number) => void;
41
+ onStateChange: (state: string) => void;
42
+ onSpeakStateChange: (state: string, client_speak_id: number | string) => void;
43
+ onRenderChange: (state: RenderState, oldState?: RenderState) => void;
44
+ onVoiceStateChange: (state: string, duration?: number, client_speak_id?: string | number) => void;
45
+ onWalkStateChange: (state: string) => void;
46
+ sendVideoInfo: (info: { name: string, body_id: number, id: number }) => void;
47
+ sendMessage: (type: string, payload: any) => void;
48
+ setAudioInfo: (info: {
49
+ sf: number;
50
+ ef: number;
51
+ ad: Uint8Array;
52
+ }) => void;
53
+ setEventData: (info: {
54
+ sf: number;
55
+ ef: number;
56
+ event: Array<any>;
57
+ }) => void;
58
+ renderFrameCallback: (frame: number) => void;
59
+ reportMessage: (message: {
60
+ code: EErrorCode;
61
+ message: string;
62
+ e?: object;
63
+ }) => void;
64
+ sendSdkPoint: (type: string, data: any, extra?: any) => void;
65
+ onFPSUpdate: (stats: FPSStats) => void;
66
+ onFrameDataUpdate?: (data: IFrameDataUpdate) => void;
67
+ }
68
+
69
+ export default class RenderScheduler {
70
+ private TAG = "[RenderScheduler]";
71
+ private dataCacheQueue: DataCacheQueue;
72
+ private sdk: XmovAvatar;
73
+ private config: CONFIG;
74
+
75
+ private avatarRenderer: AvatarRenderer;
76
+ private audioRenderer: AudioRenderer;
77
+ private uiRenderer: UIRenderer;
78
+ private composition: Composition;
79
+
80
+ private currentSpeechId: number = -1;
81
+
82
+ private resourceManager: ResourceManager;
83
+ private frameAnimationController: AnimationController | undefined;
84
+ private isStartPlay = false;
85
+ private renderState: RenderState = RenderState.init;
86
+
87
+ private onDownloadProgress: (progress: number) => void;
88
+ private onRenderChange: (state: RenderState) => void;
89
+ private decoder: ParallelDecoder;
90
+ private saveAndDownload: SaveAndDownload;
91
+ public lastSpeechId = -1;
92
+ private enableClientInterrupt: boolean = false;
93
+ private audioOnlyMode: boolean = false;
94
+ private _speedResetTimer: any = null;
95
+ private offsetFrame:number = 0;
96
+ private hasSendTimeJson: boolean = false;
97
+ private onFPSUpdate: (stats: FPSStats) => void;
98
+
99
+ private setAudioInfo: (info: {
100
+ sf: number;
101
+ ef: number;
102
+ ad: Uint8Array;
103
+ }) => void;
104
+ private setEventData: (info: {
105
+ sf: number;
106
+ ef: number;
107
+ event: Array<any>;
108
+ }) => void;
109
+ private reportMessage: (message: {
110
+ code: EErrorCode;
111
+ message: string;
112
+ e?: object;
113
+ }) => void;
114
+ private sendSdkPoint: (type: string, data: any, extra?: any) => void;
115
+ private sendMessage: (type: string, payload: any) => void;
116
+ constructor(config: CONFIG) {
117
+ this.config = config;
118
+ this.setAudioInfo = config.setAudioInfo;
119
+ this.setEventData = config.setEventData;
120
+ this.reportMessage = config.reportMessage;
121
+ this.sendSdkPoint = config.sendSdkPoint;
122
+ this.onRenderChange = config.onRenderChange;
123
+ this.sendMessage = config.sendMessage;
124
+ this.enableClientInterrupt = config.enableClientInterrupt || false;
125
+ this.audioOnlyMode = config.audioOnlyMode || false;
126
+ this.saveAndDownload = new SaveAndDownload(
127
+ "avatarData.js",
128
+ config.enableDebugger
129
+ );
130
+ this.resourceManager = config.resourceManager;
131
+ this.sdk = config.sdkInstance;
132
+ this.onDownloadProgress = config.onDownloadProgress;
133
+ this.onFPSUpdate = config.onFPSUpdate;
134
+ this.dataCacheQueue = new DataCacheQueue();
135
+ this.decoder = new ParallelDecoder({
136
+ hardwareAcceleration: config.hardwareAcceleration,
137
+ resourceManager: this.resourceManager,
138
+ saveAndDownload: this.saveAndDownload,
139
+ dataCacheQueue: this.dataCacheQueue,
140
+ reportMessage: this.reportMessage,
141
+ });
142
+
143
+ this.avatarRenderer = new AvatarRenderer({
144
+ dataCacheQueue: this.dataCacheQueue,
145
+ resourceManager: this.resourceManager,
146
+ saveAndDownload: this.saveAndDownload,
147
+ forceSyncDecoder: this.forceSyncDecoder.bind(this),
148
+ reportMessage: this.reportMessage,
149
+ sendSdkPoint: this.sendSdkPoint,
150
+ onDownloadProgress: (progress: number) => {
151
+ this.onDownloadProgress(progress);
152
+ },
153
+ onStateChange: (state: string) => {
154
+ config.onStateChange(state);
155
+ },
156
+ onRenderChange: (state: RenderState) => {
157
+ config.onRenderChange(state, this.renderState);
158
+ this.renderState = state;
159
+ },
160
+ sendVideoInfo: config.sendVideoInfo,
161
+ onError: (error: any) => {
162
+ this.sdk.onMessage(error);
163
+ },
164
+ });
165
+ this.uiRenderer = new UIRenderer({
166
+ sdk: this.sdk,
167
+ dataCacheQueue: this.dataCacheQueue,
168
+ resourceManager: this.resourceManager,
169
+ enableClientInterrupt: this.enableClientInterrupt,
170
+ onWalkStateChange: (state: string) => {
171
+ config.onWalkStateChange(state);
172
+ },
173
+ onVoiceStart: (duration: number, client_speak_id: string | number) => {
174
+ config.onVoiceStateChange("start", duration, client_speak_id);
175
+ },
176
+ onVoiceEnd: (client_speak_id: string | number, speech_id: number) => {
177
+ this.stopAudio(speech_id);
178
+ config.onVoiceStateChange("end", undefined, client_speak_id);
179
+ },
180
+ onSpeakStateChange: (state: string, client_speak_id: string | number, speech_id: number) => {
181
+ if(state === "speak_start") {
182
+ this.currentSpeechId = speech_id;
183
+ this.avatarRenderer.setInterrupt(false);
184
+ } else if(state === "speak_end") {
185
+ this.dataCacheQueue.clearSubtitleOn(speech_id);
186
+ // 仅当speak_end属于当前或更新的轮次时才设置interrupt,避免旧轮次覆盖新轮次的表情状态
187
+ if (speech_id >= this.currentSpeechId) {
188
+ this.avatarRenderer.setInterrupt(true);
189
+ }
190
+ }
191
+ config.onSpeakStateChange(state, client_speak_id);
192
+ },
193
+ sendSdkPoint: (type: string, data: any, extra?: any) => {
194
+ config.sendSdkPoint(type, data, extra);
195
+ },
196
+ });
197
+ this.audioRenderer = new AudioRenderer({
198
+ sdk: this.sdk,
199
+ dataCacheQueue: this.dataCacheQueue,
200
+ resourceManager: this.resourceManager,
201
+ });
202
+
203
+ this.composition = new Composition({
204
+ container: config.container,
205
+ avatarRenderer: this.avatarRenderer,
206
+ restRenderers: [this.uiRenderer],
207
+ audioRenderer: this.audioRenderer,
208
+ audioOnlyMode: this.audioOnlyMode,
209
+ onFPSUpdate: (stats: FPSStats) => {
210
+ this.onFPSUpdate(stats);
211
+ },
212
+ });
213
+ this.frameAnimationController = new AnimationController({
214
+ defaultSpeed: 1,
215
+ frameRate: 24,
216
+ frameCallback: (frame, timeJson) => {
217
+ // (window as any).avatarSDKLogger.log("计时器回调帧号", frame)
218
+ if(!this.hasSendTimeJson && timeJson) {
219
+ (window as any).avatarSDKLogger.error("当前帧日期与当前帧不一致:", timeJson)
220
+ // console.warn("当前帧日期与当前帧不一致:", timeJson)
221
+ this.sendMessage("client_error", timeJson);
222
+ this.hasSendTimeJson = true;
223
+ }
224
+ frame += this.offsetFrame;
225
+ config.renderFrameCallback(frame);
226
+ if (this.isStartPlay) {
227
+ this.composition.compose(frame);
228
+ this.decoder._tryStartNext();
229
+ // 帧数据回调
230
+ if (config.onFrameDataUpdate) {
231
+ const frameData = this.avatarRenderer._getCurrentFrameData(frame);
232
+ config.onFrameDataUpdate({
233
+ frameIndex: frame,
234
+ body: frameData.body,
235
+ face: frameData.face,
236
+ });
237
+ }
238
+ }
239
+ },
240
+ });
241
+ }
242
+
243
+ init() {
244
+ if (this.audioOnlyMode) {
245
+ return; // Skip WebGL initialization in audioOnlyMode
246
+ }
247
+ const mouthShapeLib = this.resourceManager.getMouthShapeLib();
248
+ this.saveAndDownload.writeFields({
249
+ resource_pack: this.resourceManager.resource_pack,
250
+ });
251
+ this.avatarRenderer.init({
252
+ char: mouthShapeLib.char_info,
253
+ LUT: null,
254
+ transform: {
255
+ offsetX: 0.0,
256
+ offsetY: 0.0,
257
+ scaleX: 1.0,
258
+ scaleY: 1.0
259
+ },
260
+ multisample: 4
261
+ });
262
+ }
263
+
264
+ /**
265
+ * 处理数据:拆解并添加到缓存队列中
266
+ * @param data TTSA 下发的数据
267
+ */
268
+ async handleData(type: EFrameDataType, data: IRawFrameData[] | SYNC_FRAME_DATA) {
269
+ if (this.audioOnlyMode) {
270
+ if (type === EFrameDataType.BODY ||
271
+ type === EFrameDataType.FACE ||
272
+ type === EFrameDataType.AA_FRAME) {
273
+ return; // Skip video data in audioOnlyMode
274
+ }
275
+ }
276
+ const currentFrameIndex = this.getCurrentFrameIndex();
277
+ switch (type) {
278
+ case EFrameDataType.BODY: {
279
+ this.decoder.clearOldQueue(currentFrameIndex);
280
+ // 根据currentFrame查找sf>currentFrame的body数据
281
+ const currentFrame = currentFrameIndex;
282
+ let mp4List = ([...data as IRawFrameData[]] as IRawBodyFrameData[]).filter((item) => item.ef > currentFrame);
283
+ (window as any).avatarSDKLogger.log(mp4List,"mp4List")
284
+ if (mp4List.length > 0) {
285
+ // 在暂停状态(隐身模式)下,不检查数据是否过期
286
+ // 因为此时帧动画暂停但数据仍在接收,可能导致帧索引与数据起始帧不同步
287
+ if (this.renderState !== RenderState.paused && currentFrameIndex && currentFrameIndex > data[0].sf && currentFrameIndex > 10) {
288
+ // 判断非初始场景下,body数据下发过期的情况
289
+ this.sdk.onMessage({
290
+ code: EErrorCode.BODY_DATA_EXPIRED,
291
+ message: `Error: 身体数据过期 cur:${currentFrameIndex}-sf:${data[0].sf}`,
292
+ e: data
293
+ });
294
+ }
295
+ // 已经插入队列的移除掉
296
+ this.decoder.decode(
297
+ mp4List,
298
+ (file: any, frame: any, index: any) => {
299
+ // createImageBitmap(frame).then((rgbBitmap) => {
300
+ if (file.start <= index && file.end >= index) {
301
+ this.dataCacheQueue._updateBodyImageBitmap({
302
+ frame,
303
+ frameIndex: file.startFrameIndex + index,
304
+ frameState: file.frameState,
305
+ id: file.id,
306
+ body_id: file.body_id,
307
+ name: file.name,
308
+ hfd: file.hfd,
309
+ sf: file.startFrameIndex,
310
+ offset: file?.x_offset[index] || 0,
311
+ });
312
+ // 视频获取完成,防止后端下发实际帧数大于需要抽的帧数,避免多余抽帧
313
+ if (file.endFrameIndex <= file.startFrameIndex + index) {
314
+ this.decoder.abortOne(
315
+ `${file.body_id ?? file.id ?? 0}_${file.name}`
316
+ );
317
+ }
318
+ }
319
+ },
320
+ );
321
+ }
322
+ break;
323
+ }
324
+ case EFrameDataType.FACE:
325
+ this.dataCacheQueue.clearOldFaceData(currentFrameIndex);
326
+ const faceData = this.handleFaceData(data as ITtsFaceFrameData[]);
327
+ if (faceData.length > 0) {
328
+ this.saveAndDownload.appendMultipleToArray("faceData", faceData);
329
+ }
330
+ this.dataCacheQueue.checkValidData(
331
+ faceData,
332
+ type
333
+ );
334
+ // https://rsjqcmnt5p.feishu.cn/wiki/UiTUwZRKRiRGitkqDc4c78eUnye
335
+ // 遍历处理完成之后的faceData,根据face_frame_type区分原始数据和实时数据,保存到不同的队列内
336
+ let realFaceData: Array<IRawFaceFrameData> = [], nowFaceData: Array<IRawFaceFrameData> = [];
337
+ for (let i = 0; i < faceData.length; i++) {
338
+ // face_frame_type 1 实时数据
339
+ if (!faceData[i]?.face_frame_type) {
340
+ realFaceData.push(faceData[i])
341
+ } else {
342
+ nowFaceData.push(faceData[i]);
343
+ }
344
+ nowFaceData.length && this.dataCacheQueue._updateFacial(nowFaceData);
345
+ realFaceData.length && this.dataCacheQueue._updateRealFacial(realFaceData);
346
+ }
347
+ break;
348
+ case EFrameDataType.AUDIO:
349
+ const audioData = data as IRawAudioFrameData[];
350
+ this.setAudioInfo({
351
+ sf: audioData[0].sf,
352
+ ef: audioData[0].ef,
353
+ ad: audioData[0].ad,
354
+ });
355
+ // 在暂停状态(隐身模式)下,不检查音频数据是否过期
356
+ // 因为此时帧动画暂停但数据仍在接收,可能导致帧索引与数据起始帧不同步
357
+ if (this.renderState !== RenderState.paused && currentFrameIndex > data[0].sf) {
358
+ const currentFrame = currentFrameIndex;
359
+ const delta = currentFrame ? currentFrame - data[0].sf : 0;
360
+ this.reportMessage({
361
+ code: EErrorCode.AUDIO_DATA_EXPIRED,
362
+ message: `Error: 音频数据过期 cur:${currentFrameIndex}-sf:${data[0].sf}`,
363
+ e: {
364
+ currentFrame: currentFrameIndex,
365
+ sf: data[0].sf,
366
+ ef: delta,
367
+ timestamp: Date.now(),
368
+ }
369
+ })
370
+ this.sendSdkPoint('audio_data_expired', {
371
+ currentFrame: currentFrameIndex,
372
+ sf: data[0].sf,
373
+ ef: delta,
374
+ timestamp: Date.now(),
375
+ });
376
+ // return;
377
+ }
378
+
379
+ // 常量定义:24帧每秒,每帧时长(ms)
380
+ const FRAME_RATE = 24;
381
+ const FRAME_DURATION_MS = 1000 / FRAME_RATE;
382
+
383
+ // 1. 解构获取数据,避免重复 data[0] 访问
384
+ const { sf: startFrame, ef: endFrame } = data[0];
385
+ const audioLenFrames = endFrame - startFrame;
386
+
387
+ // 2. 边界保护:音频长度无效直接返回
388
+ if (audioLenFrames <= 0) return;
389
+
390
+ // 3. 获取当前帧(仅调用一次)
391
+ const currentFrame = currentFrameIndex || 0;
392
+
393
+ // 4. 计算当前帧相对于音频起始帧的偏移
394
+ const frameOffset = currentFrame - startFrame;
395
+
396
+ // 5. 核心逻辑判断
397
+ if (frameOffset > audioLenFrames) {
398
+ // 超过时长,直接丢弃
399
+ return;
400
+ }
401
+
402
+ // 6. 计算播放速度 (仅在需要加速时计算)
403
+ let playbackSpeed = 1;
404
+ if (frameOffset > 0) {
405
+ // 防止除以零 (虽然前面已判断 audioLenFrames > 0,但防御性编程)
406
+ playbackSpeed = audioLenFrames / Math.max(audioLenFrames - frameOffset, 0.1);
407
+ }
408
+
409
+ // 7. 清理旧定时器,防止多个定时器冲突
410
+ if (this._speedResetTimer) {
411
+ clearTimeout(this._speedResetTimer);
412
+ }
413
+
414
+ // 8. 应用速度
415
+ this.audioRenderer.setSpeed(playbackSpeed);
416
+
417
+ // 9. 计算正确的恢复时间 (原时长 / 速度),并设置新定时器
418
+ const originalDurationMs = audioLenFrames * FRAME_DURATION_MS;
419
+ const actualDurationMs = originalDurationMs / playbackSpeed;
420
+
421
+ this._speedResetTimer = setTimeout(() => {
422
+ this.audioRenderer.setSpeed(1);
423
+ this._speedResetTimer = null; // 清理引用
424
+ }, actualDurationMs);
425
+
426
+ this.dataCacheQueue.checkValidData(data as IRawAudioFrameData[], type);
427
+
428
+ const processedAudioData = (data as IRawAudioFrameData[]).map((item: IRawAudioFrameData) => {
429
+ // raw_audio = true 时,PCM 音频下发,不进行转换
430
+ if (this.resourceManager.config.raw_audio) {
431
+ return item;
432
+ }
433
+ // raw_audio = false 时,PCM 编码音频下发,进行转换 ArrayBuffer
434
+ return {
435
+ ...item,
436
+ ad: item.ad.buffer.slice(item.ad.byteOffset, item.ad.byteOffset + item.ad.byteLength)
437
+ };
438
+ });
439
+ // 过期的音频数据,直接丢弃
440
+ if (processedAudioData[0].sid <= this.lastSpeechId) {
441
+ return;
442
+ }
443
+ if (this.resourceManager.config.raw_audio) {
444
+ (window as any).avatarSDKLogger?.log(this.TAG, 'processedAudioData[0].sid', processedAudioData[0].sid, this.currentSpeechId, this.renderState);
445
+
446
+ this.audioRenderer.updateAudioData(processedAudioData);
447
+ } else {
448
+ // 临时修复: 先缓存一份数据,用于保留开始播放的时间
449
+ this.audioRenderer._updateAudio(processedAudioData);
450
+ }
451
+ break;
452
+ case EFrameDataType.EVENT:
453
+ this.dataCacheQueue.clearOldEvent(currentFrameIndex);
454
+ const d = data as IRawEventFrameData[];
455
+ this.setEventData({
456
+ sf: d[0].sf,
457
+ ef: d[0].ef,
458
+ event: d[0].e,
459
+ });
460
+
461
+ let hasAnySpeakStart = false;
462
+ let expiredSpeakStartEv :IRawWidgetData | null = null;
463
+ let expiredVoiceStartEv :IRawWidgetData | null = null;
464
+
465
+ // 单次遍历,同时收集标记与过期事件
466
+ for (const item of d) {
467
+ const events = item.e ?? [];
468
+ // 标记队列内存在speak_start事件
469
+ if (events.some(ev => ev.type === 'speak_start')) {
470
+ hasAnySpeakStart = true;
471
+ }
472
+
473
+ // 帧过期判断,无需额外数字校验
474
+ const isFrameExpired = item.sf < currentFrameIndex;
475
+ if (!isFrameExpired) continue;
476
+ // 最多只取一个speak_start、一个voice_start,找到就不再赋值
477
+ if (!expiredSpeakStartEv) {
478
+ expiredSpeakStartEv = events.find(ev => ev.type === 'speak_start') || null;
479
+ }
480
+ if (!expiredVoiceStartEv) {
481
+ expiredVoiceStartEv = events.find(ev => ev.type === 'voice_start') || null;
482
+ }
483
+
484
+ // 两个事件都找到,直接跳出循环
485
+ if (expiredSpeakStartEv && expiredVoiceStartEv) break;
486
+ }
487
+ // 1. 新speak_start到来且音频播放中,兜底打断上一轮
488
+ if (hasAnySpeakStart && this.audioRenderer.isPlaying) {
489
+ this.reportMessage({
490
+ code: EErrorCode.EVENT_TRIGGERED,
491
+ message: 'WARNING: 有新的speak_start事件,但是上一轮仍未结束,触发兜底结束',
492
+ });
493
+ this.interrupt('new_speak_start');
494
+ }
495
+
496
+ // 2. 处理过期speak_start
497
+ if (expiredSpeakStartEv) {
498
+ const { speech_id, client_speak_id } = expiredSpeakStartEv;
499
+ this.reportMessage({
500
+ code: EErrorCode.EVENT_SPEAK_START_EXPIRED,
501
+ message: 'WARNING: 收到speak_start过期,主动重置avatarRender,调用onSpeakStateChange',
502
+ });
503
+ this.currentSpeechId = speech_id ?? 0;
504
+ this.avatarRenderer.setInterrupt(false);
505
+ this.uiRenderer.lastSpeechId = speech_id ?? 0;
506
+ this.config.onSpeakStateChange('speak_start', client_speak_id ?? 0);
507
+ }
508
+
509
+ // 3. 处理过期voice_start
510
+ if (expiredVoiceStartEv) {
511
+ this.reportMessage({
512
+ code: EErrorCode.EVENT_VOICE_START_EXPIRED,
513
+ message: 'WARNING: 收到voice_start过期,主动调用onVoiceStateChange',
514
+ });
515
+ const duration = (window as any).performanceTracker.markEnd(performanceConstant.voice_response_play, 'speak');
516
+ this.config.onVoiceStateChange('start', duration, expiredVoiceStartEv?.client_speak_id ?? '');
517
+ }
518
+
519
+ // d是一个数组,过滤出其中sf和ef为-1的事件
520
+ const specialEvents = d.filter(item => item.sf === -1 && item.ef === -1);
521
+ // 遍历这个事件,然后通过proxyEvent处理它
522
+ for (const item of specialEvents) {
523
+ for (let i = 0; i < item.e.length; i++) {
524
+ const widgetData = item.e[i];
525
+ if (WidgetDefaultRender.CUSTOM_WIDGET) {
526
+ WidgetDefaultRender.CUSTOM_WIDGET(widgetData);
527
+ } else if (WidgetDefaultRender.PROXY_WIDGET) {
528
+ WidgetDefaultRender.PROXY_WIDGET?.[widgetData.type]?.(widgetData);
529
+ }
530
+ }
531
+ }
532
+ const normalEvents = d.filter(item => item.sf !== -1);
533
+ this.dataCacheQueue._updateUiEvent(normalEvents);
534
+ break;
535
+ case EFrameDataType.SYNC_FRAME:
536
+ const syncData = data as SYNC_FRAME_DATA;
537
+ if(this.sdk.getOptions().config?.replay || syncData?.source !== FRAME_SOURCE_SOURCE) return;
538
+ const nowTime = Date.now() / 1000;
539
+ const serverTime = syncData.server_time ?? 0;
540
+ const clientTime = syncData.client_time ?? 0;
541
+ const serverClientFrame = syncData.server_client_frame ?? 0;
542
+ const rtt = nowTime - clientTime;
543
+ const correctedFrame = (serverClientFrame + (rtt / 2) * 24).toFixed(0);
544
+ this.offsetFrame += Number(correctedFrame) - Number(currentFrameIndex??0);
545
+ break;
546
+ default:
547
+ this.sdk.onMessage({
548
+ code: EErrorCode.INVALID_DATA_STRUCTURE,
549
+ message: `Error: 数据类型错误`,
550
+ e: { ...data, type }
551
+ });
552
+ }
553
+ }
554
+
555
+ setOffsetFrame(offsetFrame: number) {
556
+ this.offsetFrame += offsetFrame;
557
+ }
558
+
559
+ setVolume(volume: number) {
560
+ this.audioRenderer.setVolume(volume);
561
+ }
562
+
563
+ runStartFrameIndex() {
564
+ this.frameAnimationController?.play();
565
+ }
566
+
567
+ stateChangeHandle(e: any) {
568
+ this.dataCacheQueue.currentPlayState = e;
569
+ }
570
+
571
+ stopAudio(speech_id: number) {
572
+ this.audioRenderer.stop(speech_id);
573
+ }
574
+
575
+ render() {
576
+ if (this.audioOnlyMode) {
577
+ // audioOnlyMode 下跳过 WebGL 初始化,直接标记开始播放并触发 progress 100
578
+ this.isStartPlay = true;
579
+ this.renderState = RenderState.rendering;
580
+ this.onRenderChange(this.renderState);
581
+ this.onDownloadProgress(100);
582
+ return;
583
+ }
584
+ // 如果 pipeline 还未创建,在这里初始化(普通模式初始化时会调用这个方法)
585
+ this.avatarRenderer.createPipeline();
586
+ this.isStartPlay = true;
587
+ this.renderState = RenderState.rendering;
588
+ this.onRenderChange(this.renderState);
589
+ }
590
+
591
+ stop() {
592
+ this.isStartPlay = false;
593
+ this.renderState = RenderState.stopped;
594
+ this.onRenderChange(this.renderState);
595
+ this.composition.stop();
596
+ }
597
+
598
+ /**
599
+ * 暂停渲染(停止渲染循环和音频播放)
600
+ * 但继续接收和处理后端推送的数据并放入缓存队列,避免切换到在线时丢失数据导致丢帧
601
+ */
602
+ pauseRender(): void {
603
+ if (this.renderState === RenderState.paused) {
604
+ return;
605
+ }
606
+
607
+ // 1. 暂停动画帧循环(保留当前帧索引)
608
+ this.frameAnimationController?.pause();
609
+ this.decoder.abort();
610
+
611
+ // 2. 停止并清空音频播放(丢弃所有剩余的音频数据)
612
+ this.audioRenderer.stop(-1); // 传入 -1 清空所有音频数据
613
+
614
+ // 3. 清空所有表情数据,避免错误的旧脸部数据与当前身体数据一起渲染导致人脸分离
615
+ // 恢复渲染时会使用最新的脸部数据
616
+ this.dataCacheQueue.clearAllFaceData();
617
+
618
+ this.renderState = RenderState.paused;
619
+ this.onRenderChange(this.renderState);
620
+
621
+ (window as any).avatarSDKLogger?.log(this.TAG, `渲染已暂停(已清空表情数据,等待恢复): ${this.renderState}`);
622
+ }
623
+
624
+ /**
625
+ * 恢复渲染
626
+ */
627
+ resumeRender(): void {
628
+ if (this.renderState !== RenderState.paused) {
629
+ return;
630
+ }
631
+ // 在恢复渲染时初始化 pipeline(如果在隐身模式时还未创建)
632
+ // 这样可以避免在隐身模式时创建 pipeline 导致的 GPU 资源竞争
633
+ this.avatarRenderer.createPipeline();
634
+
635
+ // 重置表情相关状态,避免从隐身模式恢复时人脸和身体不匹配
636
+ this.avatarRenderer.resetFaceFrameState();
637
+
638
+ // 3. 恢复动画帧循环(从暂停时的帧索引继续)
639
+ // 注意:frameAnimationController 会保持暂停时的帧索引,恢复时继续
640
+
641
+ this.frameAnimationController?.play();
642
+ this.isStartPlay = true
643
+ this.forceSyncDecoder();
644
+ // 4. 恢复后立即进入渲染中状态
645
+ this.renderState = RenderState.resumed;
646
+ this.onRenderChange(this.renderState);
647
+ (window as any).avatarSDKLogger?.log(this.TAG, ' 渲染已恢复,状态: this.currentSpeechId===');
648
+ }
649
+
650
+ /**
651
+ * 切换隐身模式(暂停/恢复渲染)
652
+ */
653
+ switchInvisibleMode(): void {
654
+ if (this.renderState === RenderState.rendering || this.renderState === RenderState.resumed) {
655
+ (window as any).avatarSDKLogger?.log(this.TAG, 'switchInvisibleMode: rendering to paused');
656
+ this.pauseRender();
657
+ } else if (this.renderState === RenderState.paused) {
658
+ (window as any).avatarSDKLogger?.log(this.TAG, 'switchInvisibleMode: paused to rendering');
659
+ this.resumeRender();
660
+ } else {
661
+ // stopped 或 idle 状态不做任何操作
662
+ (window as any).avatarSDKLogger?.warn(this.TAG, `渲染状态为 ${this.renderState},无法切换隐身模式`);
663
+ }
664
+ }
665
+
666
+ /**
667
+ * 获取渲染状态
668
+ */
669
+ public getRenderState(): RenderState {
670
+ (window as any).avatarSDKLogger?.log(this.TAG, `getRenderState: ${this.renderState}`);
671
+ return this.renderState;
672
+ }
673
+
674
+ destroy() {
675
+ this.isStartPlay = false;
676
+ this.hasSendTimeJson = false;
677
+ this.offsetFrame = 0;
678
+ this._speedResetTimer = null; // 清理引用
679
+ this.frameAnimationController?.destroy();
680
+ this.frameAnimationController = undefined;
681
+ this.dataCacheQueue.destroy();
682
+ this.composition.destroy();
683
+ this.decoder.destroy();
684
+ }
685
+
686
+ handleFaceData(data: ITtsFaceFrameData[]): IRawFaceFrameData[] {
687
+ try {
688
+ const resourcePack = this.resourceManager.resource_pack;
689
+ const framedata_proto_version = this.resourceManager.getConfig().framedata_proto_version;
690
+ const blendshape_map = [...resourcePack.blendshape_map ?? []];
691
+ const face_list: any[] = data.map((item) => {
692
+ let bsw_list: number[][] = [];
693
+ const { body_id, sf, ef, s: state, id, js, bsw, ms, face_frame_type, asf, aef, n, cs} = item;
694
+ let mesh: any[] = [];
695
+ if (blendshape_map?.length > 0) {
696
+ for (let i = 0; i < blendshape_map.length; i++) {
697
+ const blendshape_map_item = blendshape_map[i];
698
+ if (blendshape_map_item?.length > 0) {
699
+ for (let j = 0; j < blendshape_map_item.length; j++) {
700
+ if (!bsw_list[i]) {
701
+ bsw_list[i] = [];
702
+ }
703
+ bsw_list[i][j] = bsw[blendshape_map_item[j]];
704
+ }
705
+ }
706
+ }
707
+ if (!framedata_proto_version) {
708
+ mesh = Array.from({ length: ms.length }, (_, index) => ({
709
+ blendshapeWeights: bsw_list[index],
710
+ textureModelIndex: ms[index]?.[0] ?? 0,
711
+ texturePCAWeights: ms[index]?.[1] ?? [],
712
+ }));
713
+ } else {
714
+ mesh = Array.from({ length: ms.length }, (_, index) => ({
715
+ blendshapeWeights: bsw_list[index],
716
+ textureModelIndex: ms[index]?.index ?? 0,
717
+ texturePCAWeights: ms[index]?.weights ?? [],
718
+ }));
719
+ }
720
+ } else {
721
+ if (!framedata_proto_version) {
722
+ mesh = Array.from({ length: ms.length }, (_, index) => ({
723
+ // blendshape_map 内容如[[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49],[44,45,8,10,12],[44,45,9,11,13],[44,45]]
724
+ // 在遍历时,需要根据 blendshape_map 的索引,将 bsw 中的值替换为 blendshape_map 中索引的值
725
+ blendshapeWeights: index === 0 ? bsw : [],
726
+ textureModelIndex: ms[index]?.[0] ?? 0, // 增加可选链和默认值,防止越界
727
+ texturePCAWeights: ms[index]?.[1] ?? [],
728
+ }));
729
+ } else {
730
+ mesh = Array.from({ length: ms.length }, (_, index) => ({
731
+ // blendshape_map 内容如[[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49],[44,45,8,10,12],[44,45,9,11,13],[44,45]]
732
+ // 在遍历时,需要根据 blendshape_map 的索引,将 bsw 中的值替换为 blendshape_map 中索引的值
733
+ blendshapeWeights: index === 0 ? bsw : [],
734
+ textureModelIndex: ms[index]?.index ?? 0, // 增加可选链和默认值,防止越界
735
+ texturePCAWeights: ms[index]?.weights ?? [],
736
+ }));
737
+
738
+ }
739
+ }
740
+ let movableJointTransforms: any[] = []
741
+ if (!framedata_proto_version) {
742
+ movableJointTransforms = js.map((joint) =>
743
+ formatMJT(joint[0], joint[1])
744
+ );
745
+ } else {
746
+ movableJointTransforms = js.map((joint) =>
747
+ formatMJT(joint.translate, joint.rotate)
748
+ );
749
+ }
750
+ return {
751
+ body_id,
752
+ frameIndex: sf,
753
+ sf,
754
+ ef,
755
+ asf,
756
+ aef,
757
+ n,
758
+ cs,
759
+ state,
760
+ id,
761
+ face_frame_type,
762
+ FaceFrameData: {
763
+ mesh,
764
+ movableJointTransforms,
765
+ blendshapeWeights: bsw,
766
+ },
767
+ };
768
+ });
769
+ return face_list;
770
+ } catch (error) {
771
+ this.sdk.onMessage({
772
+ code: EErrorCode.FACE_PROCESSING_ERROR,
773
+ message: `Error: 表情数据处理失败`,
774
+ e: error,
775
+ });
776
+ return [];
777
+ }
778
+ }
779
+ forceSyncDecoder() {
780
+ const currentFrameIndex = this.getCurrentFrameIndex();
781
+ if (currentFrameIndex) {
782
+ this.decoder.syncDecode(currentFrameIndex);
783
+ }
784
+ }
785
+ _reload() {
786
+ this.decoder._reload()
787
+ }
788
+ _getResumeInfo() {
789
+ const frame = this.getCurrentFrameIndex();
790
+ return this.avatarRenderer._getCurrentBodyFrameInfo(frame || 0)
791
+ }
792
+
793
+ sendVoiceEnd() {
794
+ const currentFrameIndex = (this.getCurrentFrameIndex() ?? 0) + 1;
795
+ this.dataCacheQueue._updateUiEvent([{
796
+ "id": 0,
797
+ "s": "",
798
+ "sf": currentFrameIndex,
799
+ "ef": currentFrameIndex + 1,
800
+ "e": [
801
+ {
802
+ "type": "speak_end",
803
+ "speech_id": this.uiRenderer.lastSpeechId ?? -1,
804
+ },
805
+ {
806
+ "type": "voice_end",
807
+ "speech_id": this.uiRenderer.lastSpeechId ?? -1,
808
+ },
809
+ ]
810
+ }])
811
+ }
812
+
813
+ _offlineMode() {
814
+ this.dataCacheQueue._clearEvent();
815
+ const currentFrame = this.getCurrentFrameIndex() ?? 0;
816
+ this.dataCacheQueue._updateUiEvent([{
817
+ "id": 0,
818
+ "s": "",
819
+ "sf": currentFrame + 1,
820
+ "ef": currentFrame + 2,
821
+ "e": [
822
+ {
823
+ "type": "subtitle_off",
824
+ "speech_id": this.uiRenderer.lastSpeechId ?? -1,
825
+ },
826
+ ]
827
+ }])
828
+ this.audioRenderer.pause()
829
+ const frame = this.getCurrentFrameIndex();
830
+ if (frame) {
831
+ this.decoder._offLineMode(this.resourceManager._getOfflineIdle(), frame);
832
+ }
833
+ }
834
+ _offlineRun() {
835
+ this.decoder._offlineRun()
836
+ this.dataCacheQueue.clearAllFaceData();
837
+ }
838
+
839
+ ttsaStateChangeHandle(state: StateChangeInfo) {
840
+ this.dataCacheQueue.currentTtsaState = state;
841
+ }
842
+
843
+ resume() {
844
+ this.audioRenderer.resume();
845
+ }
846
+
847
+ /**
848
+ * 设置数字人canvas的显隐状态
849
+ * @param visible 是否可见
850
+ */
851
+ setAvatarCanvasVisible(visible: boolean) {
852
+ this.avatarRenderer.setCanvasVisibility(visible);
853
+ }
854
+ setCharacterCanvasLayout(layout?: Layout) {
855
+ this.avatarRenderer.setCharacterCanvasAnchor(layout);
856
+ }
857
+
858
+ setWalkConfig(walkConfig: WalkConfig) {
859
+ this.avatarRenderer.setWalkConfig(walkConfig);
860
+ }
861
+
862
+ interrupt(type?: string): number{
863
+ const start = Date.now();
864
+ // 插入中断字幕和语音结束事件
865
+ const speech_id = this.audioRenderer.speech_id;
866
+ if(type === "in_offline_mode") {
867
+ this.lastSpeechId = -1;
868
+ } else {
869
+ this.lastSpeechId = speech_id;
870
+ }
871
+ if(type !== "in_offline_mode") {
872
+ this.avatarRenderer.setInterrupt(true);
873
+ }
874
+ this.dataCacheQueue._clearAudio(speech_id);
875
+ this.dataCacheQueue.clearSubtitleOn(speech_id);
876
+ const currentFrame = this.getCurrentFrameIndex() ?? 0;
877
+ this.dataCacheQueue._updateUiEvent([{
878
+ "id": 0,
879
+ "s": "",
880
+ "sf": currentFrame + 1,
881
+ "ef": currentFrame + 2,
882
+ "e": [
883
+ {
884
+ "type": "subtitle_off",
885
+ "speech_id": speech_id ?? -1,
886
+ },
887
+ ]
888
+ }])
889
+
890
+ // 中断音频播放
891
+ if(type !== "in_offline_mode") {
892
+ this.audioRenderer.stop(speech_id);
893
+ }
894
+ this.uiRenderer.setInterrupt(speech_id)
895
+ return Date.now() - start;
896
+ }
897
+
898
+ seekToFrame(frame: number) {
899
+ this.frameAnimationController?.updateStartTime(frame);
900
+ }
901
+
902
+ getCurrentFrameIndex() {
903
+ return (this.frameAnimationController?.getCurrentFrame()??0) + this.offsetFrame;
904
+ }
905
+
906
+ getSpeechId() {
907
+ return this.audioRenderer.speech_id;
908
+ }
909
+
910
+ getAudioRenderer(): AudioRenderer {
911
+ return this.audioRenderer;
912
+ }
913
+
914
+ // ── 测试用方法:模拟 WebGL 上下文丢失 ─────────────────────────────
915
+
916
+ /**
917
+ * [测试用] 模拟 WebGL 上下文丢失
918
+ */
919
+ public simulateContextLoss(restoreAfterMs: number = 5000, forceFail: boolean = false): void {
920
+ this.avatarRenderer.simulateContextLoss(restoreAfterMs, forceFail);
921
+ }
922
+
923
+ /**
924
+ * [测试用] 获取当前恢复状态信息
925
+ */
926
+ public getRecoveryStatus() {
927
+ return this.avatarRenderer.getRecoveryStatus();
928
+ }
929
+
930
+ /**
931
+ * [测试用] 批量测试分阶段恢复
932
+ */
933
+ public async testStagedRecovery(): Promise<void> {
934
+ return this.avatarRenderer.testStagedRecovery();
935
+ }
936
+
937
+ /**
938
+ * 手动重启 WebGL:创建新 canvas + 新 GLDevice + 新 GLPipeline
939
+ * 用于分阶段恢复全部失败后的手动恢复
940
+ * @returns 是否成功
941
+ */
942
+ public restartWebGL(): boolean {
943
+ return this.avatarRenderer.restartWebGL();
944
+ }
945
+ }