@xmov/avatar 2.0.0-alpha.46 → 2.0.0-alpha.48
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/dist/baseRender/AvatarRenderer.d.ts +0 -1
- package/dist/control/RenderScheduler.d.ts +7 -0
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.modern.js +1 -1
- package/dist/index.modern.js.map +1 -1
- package/dist/index.module.js +1 -1
- package/dist/index.module.js.map +1 -1
- package/dist/index.umd.js +1 -1
- package/dist/index.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/baseRender/AvatarRenderer.ts +11 -20
- package/src/control/RenderScheduler.ts +10 -3
- package/src/index.ts +25 -7
- package/src/modules/ResourceManager.ts +12 -3
- package/src/modules/decoder.ts +34 -1
package/package.json
CHANGED
|
@@ -19,16 +19,11 @@ type Option = {
|
|
|
19
19
|
resourceManager: ResourceManager;
|
|
20
20
|
saveAndDownload: SaveAndDownload;
|
|
21
21
|
forceSyncDecoder: () => void;
|
|
22
|
-
reportMessage: (message: {
|
|
23
|
-
code: EErrorCode;
|
|
24
|
-
message: string;
|
|
25
|
-
e?: object;
|
|
26
|
-
}) => void;
|
|
22
|
+
reportMessage: (message: { code: EErrorCode; message: string; e?: object }) => void;
|
|
27
23
|
onDownloadProgress: (progress: number) => void;
|
|
28
24
|
onStateChange: (state: string) => void;
|
|
29
25
|
onRenderChange: (state: RenderState) => void;
|
|
30
26
|
sendVideoInfo: (info: { name: string; body_id: number; id: number }) => void;
|
|
31
|
-
onError: (error: any) => void;
|
|
32
27
|
sendSdkPoint: (point: string, data: any) => void;
|
|
33
28
|
};
|
|
34
29
|
export default class AvatarRender {
|
|
@@ -989,13 +984,13 @@ export default class AvatarRender {
|
|
|
989
984
|
} else {
|
|
990
985
|
if(this.lastRealFaceFrame === -1) {
|
|
991
986
|
faceFrame = curRealFaceData;
|
|
992
|
-
// this.options.
|
|
987
|
+
// this.options.reportMessage({
|
|
993
988
|
// code: EErrorCode.RENDER_FACE_ERROR,
|
|
994
989
|
// message: `第${frameIndex}帧 实时面部数据为空,原始数据渲染`,
|
|
995
990
|
// e: JSON.stringify({ bodyFrame, faceFrame }),
|
|
996
991
|
// });
|
|
997
992
|
} else {
|
|
998
|
-
// this.options.
|
|
993
|
+
// this.options.reportMessage({
|
|
999
994
|
// code: EErrorCode.RENDER_FACE_ERROR,
|
|
1000
995
|
// message: `第${frameIndex}帧 实时面部数据为空,插值渲染`,
|
|
1001
996
|
// e: JSON.stringify({ bodyFrame, faceFrame }),
|
|
@@ -1036,30 +1031,26 @@ export default class AvatarRender {
|
|
|
1036
1031
|
// 判断bodyFrame和faceFrame是否为空
|
|
1037
1032
|
if (!bodyFrame) {
|
|
1038
1033
|
this.allLostFrameCount++;
|
|
1039
|
-
this.options.
|
|
1034
|
+
this.options.reportMessage({
|
|
1040
1035
|
code: EErrorCode.RENDER_BODY_ERROR,
|
|
1041
1036
|
message: `Error: 第${frameIndex}帧 bodyFrame为空`,
|
|
1042
|
-
e:
|
|
1037
|
+
e: { bodyFrame, faceFrame },
|
|
1043
1038
|
});
|
|
1044
1039
|
} else if (!faceFrame) {
|
|
1045
1040
|
this.allLostFaceFrameCount++;
|
|
1046
|
-
this.options.
|
|
1041
|
+
this.options.reportMessage({
|
|
1047
1042
|
code: EErrorCode.RENDER_FACE_ERROR,
|
|
1048
1043
|
message: `Error: 第${frameIndex}帧 faceFrame为空`,
|
|
1049
|
-
e:
|
|
1044
|
+
e: { bodyFrame, faceFrame },
|
|
1050
1045
|
});
|
|
1051
1046
|
}
|
|
1052
1047
|
if(this.allLostFrameCount === this.maxLostFrameCount) {
|
|
1053
1048
|
this.options.reportMessage({
|
|
1054
1049
|
code: EErrorCode.RENDER_DATA_MISSING,
|
|
1055
|
-
message: `连续${this.maxLostFrameCount}帧
|
|
1050
|
+
message: `连续${this.maxLostFrameCount}帧 身体渲染数据缺失`,
|
|
1051
|
+
e: { bodyFrame, faceFrame },
|
|
1056
1052
|
});
|
|
1057
1053
|
// 丢失过多帧,达到最大允许丢失帧数后,触发解码器对齐到最新的一帧
|
|
1058
|
-
this.options.onError({
|
|
1059
|
-
code: EErrorCode.RENDER_DATA_MISSING,
|
|
1060
|
-
message: `Error: 连续${this.maxLostFrameCount}帧 身体渲染数据缺失`,
|
|
1061
|
-
e: JSON.stringify({ bodyFrame, faceFrame }),
|
|
1062
|
-
});
|
|
1063
1054
|
this.forceSyncDecoder();
|
|
1064
1055
|
this.allLostFrameCount = 0;
|
|
1065
1056
|
}
|
|
@@ -1180,10 +1171,10 @@ export default class AvatarRender {
|
|
|
1180
1171
|
rendered = true;
|
|
1181
1172
|
} catch (error) {
|
|
1182
1173
|
(window as any).avatarSDKLogger.error(this.TAG, "渲染帧失败:", error);
|
|
1183
|
-
this.options.
|
|
1174
|
+
this.options.reportMessage({
|
|
1184
1175
|
code: EErrorCode.WebGL_RENDER_ERROR,
|
|
1185
1176
|
message: `Error: 第${frameIndex}帧 webgl渲染异常`,
|
|
1186
|
-
e:
|
|
1177
|
+
e: { bodyFrame, faceFrame, error },
|
|
1187
1178
|
});
|
|
1188
1179
|
} finally {
|
|
1189
1180
|
// 渲染完成后再关闭VideoFrame
|
|
@@ -158,9 +158,6 @@ export default class RenderScheduler {
|
|
|
158
158
|
this.renderState = state;
|
|
159
159
|
},
|
|
160
160
|
sendVideoInfo: config.sendVideoInfo,
|
|
161
|
-
onError: (error: any) => {
|
|
162
|
-
this.sdk.onMessage(error);
|
|
163
|
-
},
|
|
164
161
|
});
|
|
165
162
|
this.uiRenderer = new UIRenderer({
|
|
166
163
|
sdk: this.sdk,
|
|
@@ -857,6 +854,16 @@ export default class RenderScheduler {
|
|
|
857
854
|
this.audioRenderer.resume();
|
|
858
855
|
}
|
|
859
856
|
|
|
857
|
+
/**
|
|
858
|
+
* app 切后台时暂停音频并清空对齐状态(复用断网离线时的 pause())
|
|
859
|
+
* 不能保留缓存续播:Android 后台视频帧号持续递增而音频停留在暂停位置,
|
|
860
|
+
* 恢复后音画基准脱节;且后台期间可能下发新 speak 数据,旧缓存与新 sf 冲突。
|
|
861
|
+
* 恢复后依赖新下发音频的 sf 作为 firstFrameIndex 重新对齐
|
|
862
|
+
*/
|
|
863
|
+
pauseForBackground() {
|
|
864
|
+
this.audioRenderer.pause();
|
|
865
|
+
}
|
|
866
|
+
|
|
860
867
|
/**
|
|
861
868
|
* 设置数字人canvas的显隐状态
|
|
862
869
|
* @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
|
-
//
|
|
1044
|
-
//
|
|
1045
|
-
//
|
|
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
|
-
|
|
1048
|
-
|
|
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);
|
|
@@ -305,9 +305,15 @@ export default class ResourceManager {
|
|
|
305
305
|
}, 100);
|
|
306
306
|
// http 请求算 10 进度
|
|
307
307
|
const result = await this.startSession();
|
|
308
|
-
|
|
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 || []
|
package/src/modules/decoder.ts
CHANGED
|
@@ -279,12 +279,32 @@ export default class ParallelDecoder {
|
|
|
279
279
|
|
|
280
280
|
_startWorker(file: bodyFile) {
|
|
281
281
|
const taskId = this.currentTaskId;
|
|
282
|
-
|
|
282
|
+
let worker: Worker;
|
|
283
|
+
try {
|
|
284
|
+
worker = new Worker(workerURL);
|
|
285
|
+
} catch (err) {
|
|
286
|
+
this.reportMessage({
|
|
287
|
+
code: EErrorCode.INIT_WORKER_ERROR,
|
|
288
|
+
message: '初始化视频抽帧Worker失败',
|
|
289
|
+
e: err as object,
|
|
290
|
+
});
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
283
293
|
const id = `${file.body_id ?? file.id ?? 0}_${file.name}`;
|
|
284
294
|
const data = file.data;
|
|
285
295
|
const dataCopy = new ArrayBuffer(data.byteLength);
|
|
286
296
|
new Uint8Array(dataCopy).set(new Uint8Array(data));
|
|
287
297
|
|
|
298
|
+
// 监听 Worker 运行时错误(脚本加载/解析失败等)
|
|
299
|
+
worker.addEventListener("error", (err) => {
|
|
300
|
+
if (taskId !== this.currentTaskId) return;
|
|
301
|
+
this.reportMessage({
|
|
302
|
+
code: EErrorCode.INIT_WORKER_ERROR,
|
|
303
|
+
message: '视频抽帧Worker运行时错误',
|
|
304
|
+
e: err as object,
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
|
|
288
308
|
const onMessage = (e: MessageEvent) => {
|
|
289
309
|
if (taskId !== this.currentTaskId) return; // 只处理当前任务
|
|
290
310
|
if (e.data.type === "frame") {
|
|
@@ -335,6 +355,19 @@ export default class ParallelDecoder {
|
|
|
335
355
|
}
|
|
336
356
|
this._tryStartNext();
|
|
337
357
|
} else if (e.data.type === "error") {
|
|
358
|
+
// 根据 Worker 上报的 error 内容区分错误码
|
|
359
|
+
// Worker 中有两处 postMessage({ type: 'error' }):
|
|
360
|
+
// 1. VideoDecoder error 回调 → 视频抽帧错误 (VIDEO_FRAME_EXTRACT_ERROR)
|
|
361
|
+
// 2. 解码配置不支持 → 视频流处理错误 (PROCESS_VIDEO_STREAM_ERROR)
|
|
362
|
+
const errorMsg = e.data.error ?? '';
|
|
363
|
+
const code = errorMsg.includes('不支持')
|
|
364
|
+
? EErrorCode.PROCESS_VIDEO_STREAM_ERROR
|
|
365
|
+
: EErrorCode.VIDEO_FRAME_EXTRACT_ERROR;
|
|
366
|
+
this.reportMessage({
|
|
367
|
+
code,
|
|
368
|
+
message: errorMsg || '视频解码错误',
|
|
369
|
+
e: { fileName: file.name, error: errorMsg } as object,
|
|
370
|
+
});
|
|
338
371
|
const task = this.tasks.get(id);
|
|
339
372
|
if (task) {
|
|
340
373
|
task.status = "error";
|