@xmov/avatar 2.0.0-alpha.47 → 2.0.0-alpha.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmov/avatar",
3
- "version": "2.0.0-alpha.47",
3
+ "version": "2.0.0-alpha.49",
4
4
  "description": "魔珐数字人 JS-SDK",
5
5
  "type": "module",
6
6
  "source": "src/index.ts",
@@ -338,9 +338,10 @@ export default class RenderScheduler {
338
338
  } else {
339
339
  nowFaceData.push(faceData[i]);
340
340
  }
341
- nowFaceData.length && this.dataCacheQueue._updateFacial(nowFaceData);
342
- realFaceData.length && this.dataCacheQueue._updateRealFacial(realFaceData);
343
341
  }
342
+ // 分类完成后一次性入队,避免循环内对累加数组重复 push 导致队列数据重复
343
+ nowFaceData.length && this.dataCacheQueue._updateFacial(nowFaceData);
344
+ realFaceData.length && this.dataCacheQueue._updateRealFacial(realFaceData);
344
345
  break;
345
346
  case EFrameDataType.AUDIO:
346
347
  const audioData = data as IRawAudioFrameData[];
@@ -493,6 +494,15 @@ export default class RenderScheduler {
493
494
  message: 'WARNING: 有新的speak_start事件,但是上一轮仍未结束,触发兜底结束',
494
495
  });
495
496
  this.interrupt('new_speak_start');
497
+ // 新 speak_start 已到来,立即复位 interrupt 并更新轮次状态,
498
+ // 避免依赖 UIRenderer 在 sf 帧处理事件时(sf==当前帧且事件在 compose 后被处理)被漏处理,
499
+ // 导致新轮次实际说话期间仍被强制渲染为闭嘴表情
500
+ const currentSpeakStart = d.flatMap(item => item.e ?? []).find(ev => ev.type === 'speak_start');
501
+ if (currentSpeakStart) {
502
+ this.currentSpeechId = currentSpeakStart.speech_id ?? 0;
503
+ this.avatarRenderer.setInterrupt(false);
504
+ this.uiRenderer.lastSpeechId = currentSpeakStart.speech_id ?? 0;
505
+ }
496
506
  }
497
507
 
498
508
  // 2. 处理过期speak_start
@@ -854,6 +864,16 @@ export default class RenderScheduler {
854
864
  this.audioRenderer.resume();
855
865
  }
856
866
 
867
+ /**
868
+ * app 切后台时暂停音频并清空对齐状态(复用断网离线时的 pause())
869
+ * 不能保留缓存续播:Android 后台视频帧号持续递增而音频停留在暂停位置,
870
+ * 恢复后音画基准脱节;且后台期间可能下发新 speak 数据,旧缓存与新 sf 冲突。
871
+ * 恢复后依赖新下发音频的 sf 作为 firstFrameIndex 重新对齐
872
+ */
873
+ pauseForBackground() {
874
+ this.audioRenderer.pause();
875
+ }
876
+
857
877
  /**
858
878
  * 设置数字人canvas的显隐状态
859
879
  * @param visible 是否可见
package/src/index.ts CHANGED
@@ -1040,12 +1040,28 @@ export default class XmovAvatar {
1040
1040
 
1041
1041
  visibilitychange() {
1042
1042
  if (document.hidden) {
1043
- // this.offlineMode()
1044
- // tab 回来时,强制同步解帧队列
1045
- // this.renderScheduler.forceSyncDecoder();
1043
+ // 进入后台:暂停渲染循环和解码器(省电)、清空音频与表情数据
1044
+ // 仅渲染中暂停(resumeRender 要求 paused 状态才能恢复),
1045
+ // 其他状态(init/stopped/隐身 paused)只停止音频,避免强制暂停后无法恢复
1046
+ const rs = this.renderScheduler?.getRenderState();
1047
+ if (rs === RenderState.rendering || rs === RenderState.resumed) {
1048
+ this.renderScheduler?.pauseRender();
1049
+ } else {
1050
+ this.renderScheduler?.pauseForBackground();
1051
+ }
1046
1052
  } else {
1047
- if (this.ttsa?.getStatus()) {
1048
- this.renderScheduler.forceSyncDecoder();
1053
+ // 回到前台:若处于暂停态则恢复渲染(内部已 forceSyncDecoder 对齐最新帧)
1054
+ if (this.renderScheduler?.getRenderState() === RenderState.paused) {
1055
+ this.renderScheduler?.resumeRender();
1056
+ } else if (this.ttsa?.getStatus()) {
1057
+ // 未暂停但连接正常:兜底同步解帧队列
1058
+ this.renderScheduler?.forceSyncDecoder();
1059
+ }
1060
+ // WebSocket 在后台断开且已进入离线模式,网络可用则主动触发重连,
1061
+ // 否则用户切回前台续播时无响应且无任何报错
1062
+ if (!this.ttsa?.getStatus() && this.status === AvatarStatus.offline && NetworkMonitor.ONLINE) {
1063
+ (window as any).avatarSDKLogger.warn(this.TAG, "切回前台发现连接已断开,主动触发重连");
1064
+ this.onlineMode();
1049
1065
  }
1050
1066
  // tab 切换回来时,恢复音频捕获的 AudioContext
1051
1067
  if (this.captureAudioCtx && this.captureAudioCtx.state === 'suspended') {
@@ -1351,7 +1367,7 @@ export default class XmovAvatar {
1351
1367
  // 薄客户端模式:通知 server 端销毁,然后清理本地资源
1352
1368
  if (this._isClientMode) {
1353
1369
  this._apiForwarder?.destroy(stop_reason);
1354
- this.destroyClient();
1370
+ await this.destroyClient();
1355
1371
  this.onStatusChange(AvatarStatus.close);
1356
1372
  return;
1357
1373
  }
@@ -1359,8 +1375,8 @@ export default class XmovAvatar {
1359
1375
  this.ttsa?.sendSdkPoint('close_session', {
1360
1376
  reason: stop_reason,
1361
1377
  });
1362
- this.destroyClient();
1363
1378
  const res = await this.resourceManager.stopSession(stop_reason);
1379
+ await this.destroyClient();
1364
1380
  if(res) {
1365
1381
  this.onStatusChange(AvatarStatus.close);
1366
1382
  }
@@ -1481,6 +1497,8 @@ export default class XmovAvatar {
1481
1497
  });
1482
1498
  this.incompleteInitializationCleanupPromise = (async () => {
1483
1499
  try {
1500
+ // 先等待房间释放完成,再清理本地资源,避免房间泄漏
1501
+ await stopSessionPromise;
1484
1502
  await this.destroyClient();
1485
1503
  } catch (error) {
1486
1504
  (window as any).avatarSDKLogger.warn(this.TAG, '清理初始化未完成的本地资源时出错', error);
@@ -205,7 +205,7 @@ export default class ResourceManager {
205
205
  private isProcessingVideo = false; // 防止在视频处理过程中清理缓存
206
206
  private cacheServer?: CacheManager;
207
207
  private startSessionTimer: any = null;
208
- private audioOnlyMode: boolean;
208
+ private audioOnlyMode: boolean = false;
209
209
 
210
210
  // 新增:正在下载的视频跟踪
211
211
  private downloadingVideos = new Map<string, Promise<ArrayBuffer | undefined>>();
@@ -305,9 +305,15 @@ export default class ResourceManager {
305
305
  }, 100);
306
306
  // http 请求算 10 进度
307
307
  const result = await this.startSession();
308
- // 如果在 startSession 过程中被销毁了,直接返回,避免继续加载资源
308
+ const res = result as unknown as ISessionResponse;
309
+ // 先记录 session_id,确保后续 stopSession 能正确释放房间
310
+ if (res?.session_id) {
311
+ this.session_id = res.session_id;
312
+ }
313
+ // 如果在 startSession 过程中被销毁了,释放刚创建的房间并返回,避免房间泄漏
309
314
  if (this.destroyed) {
310
315
  (window as any).avatarSDKLogger.warn(this.TAG, "sdk destroyed during load, aborting after startSession");
316
+ await this.stopSession("destroy_during_init");
311
317
  return null;
312
318
  }
313
319
  if (!result) {
@@ -320,8 +326,6 @@ export default class ResourceManager {
320
326
  this.startSessionTimer = null;
321
327
  this.progress = 15;
322
328
  onDownloadProgress?.(this.progress);
323
- const res = result as unknown as ISessionResponse;
324
- this.session_id = res?.session_id;
325
329
  this.resource_pack = res.resource_pack;
326
330
  this.config = res.config as any;
327
331
  this.offlineIdle = res.resource_pack?.offline_idle || []
@@ -842,6 +846,11 @@ export default class ResourceManager {
842
846
  }
843
847
  const res = result as unknown as ISessionResponse;
844
848
  this.session_id = res?.session_id;
849
+ // 重连过程中被销毁,释放刚创建的房间,避免房间泄漏
850
+ if (this.destroyed) {
851
+ await this.stopSession("destroy_during_reload");
852
+ return null;
853
+ }
845
854
  this.resource_pack = res.resource_pack;
846
855
  this.config = res.config as any;
847
856
  this.offlineIdle = res.resource_pack?.offline_idle || []