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

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.49",
3
+ "version": "2.0.0-alpha.51",
4
4
  "description": "魔珐数字人 JS-SDK",
5
5
  "type": "module",
6
6
  "source": "src/index.ts",
@@ -493,11 +493,29 @@ export default class RenderScheduler {
493
493
  code: EErrorCode.EVENT_TRIGGERED,
494
494
  message: 'WARNING: 有新的speak_start事件,但是上一轮仍未结束,触发兜底结束',
495
495
  });
496
- this.interrupt('new_speak_start');
496
+ const currentSpeakStart = d.flatMap(item => item.e ?? []).find(ev => ev.type === 'speak_start');
497
+ const newSpeakId = currentSpeakStart?.speech_id ?? 0;
498
+ const playingSpeechId = this.audioRenderer.speech_id;
499
+ // 仅当正在播放的确实是上一轮(sid 小于新一轮)时才打断旧轮次。
500
+ // 若事件晚于音频到达(playingSpeechId 已是新一轮 sid),此时播放的就是新一轮,
501
+ // 不能用 interrupt()(其目标取 audioRenderer.speech_id),否则会误清新一轮的音频队列、
502
+ // 字幕事件,并因 lastSpeechId/oldSpeechId 更新导致后续音视频帧全部被丢弃,
503
+ // 表现为"有口型、无字幕、无音频"。
504
+ if (playingSpeechId !== -1 && playingSpeechId < newSpeakId) {
505
+ this.interrupt('new_speak_start', playingSpeechId);
506
+ // stop() 内部有 oldSpeechId >= speech_id 的防重入守卫:当 speech_id 停留在
507
+ // 异常残留值(如初始值 0,长会话中 isPlaying 卡死为 true)时 stop 会被拦截,
508
+ // isPlaying/speech_id 无法复位,导致新一轮音频在 _updateAudio 中永远走缓存分支
509
+ // 不播放(日志表现:缓存数据长度持续增长且无"开始播放")。
510
+ // 这里检测到 stop 未生效(speech_id 未复位为 -1)时,用 pause() 强制复位,
511
+ // pause 会 flush 已缓存音频并把 speech_id/oldSpeechId 重置,新一轮可正常起播。
512
+ if (this.audioRenderer.speech_id !== -1) {
513
+ this.audioRenderer.pause();
514
+ }
515
+ }
497
516
  // 新 speak_start 已到来,立即复位 interrupt 并更新轮次状态,
498
517
  // 避免依赖 UIRenderer 在 sf 帧处理事件时(sf==当前帧且事件在 compose 后被处理)被漏处理,
499
518
  // 导致新轮次实际说话期间仍被强制渲染为闭嘴表情
500
- const currentSpeakStart = d.flatMap(item => item.e ?? []).find(ev => ev.type === 'speak_start');
501
519
  if (currentSpeakStart) {
502
520
  this.currentSpeechId = currentSpeakStart.speech_id ?? 0;
503
521
  this.avatarRenderer.setInterrupt(false);
@@ -889,10 +907,13 @@ export default class RenderScheduler {
889
907
  this.avatarRenderer.setWalkConfig(walkConfig);
890
908
  }
891
909
 
892
- interrupt(type?: string): number{
910
+ // targetSpeechId: 显式指定要打断的轮次 sid。缺省时取 audioRenderer.speech_id(当前播放轮次)。
911
+ // 当事件晚于音频到达时,audioRenderer.speech_id 可能已经切换到新一轮,此时必须显式传旧 sid,
912
+ // 避免误清新一轮的音频与字幕。
913
+ interrupt(type?: string, targetSpeechId?: number): number{
893
914
  const start = Date.now();
894
915
  // 插入中断字幕和语音结束事件
895
- const speech_id = this.audioRenderer.speech_id;
916
+ const speech_id = targetSpeechId ?? this.audioRenderer.speech_id;
896
917
  if(type === "in_offline_mode") {
897
918
  this.lastSpeechId = -1;
898
919
  } else {
@@ -110,17 +110,27 @@ function checkAudioSystemSync(): ICapabilityCheckItem {
110
110
 
111
111
  /**
112
112
  * 同步检查 WebCodecs API 是否存在(不探测具体编解码器)
113
+ *
114
+ * 与 streaming-video.ts 解码策略一致标注平台解码路径:
115
+ * - Android(含 OpenHarmony/ArkWeb):prefer-software 软解码
116
+ * - 其余环境:默认配置(硬解码)
117
+ * 实际解码配置支持度由异步 checkWebCodecsDetailed 探测
113
118
  */
114
119
  function checkWebCodecsSync(): ICapabilityCheckItem {
115
120
  const hasVideoDecoder = typeof (window as any).VideoDecoder !== 'undefined';
116
- const supported = hasVideoDecoder;
121
+ const userAgent = (navigator.userAgent || '').toLowerCase();
122
+ const isAndroid = /android/.test(userAgent) || /openharmony|arkweb/.test(userAgent);
117
123
 
118
124
  return {
119
125
  name: 'webcodecs',
120
- supported,
126
+ supported: hasVideoDecoder,
121
127
  severity: 'fatal',
122
- reason: supported ? undefined : '浏览器不支持 WebCodecs VideoDecoder API',
123
- detail: { hasVideoDecoder },
128
+ reason: hasVideoDecoder ? undefined : '浏览器不支持 WebCodecs VideoDecoder API',
129
+ detail: {
130
+ hasVideoDecoder,
131
+ isAndroid,
132
+ hardwareAcceleration: isAndroid ? 'prefer-software' : 'default',
133
+ },
124
134
  };
125
135
  }
126
136
 
@@ -186,13 +196,17 @@ export function checkCapabilitySync(audioOnlyMode = false): ICapabilityCheckRepo
186
196
 
187
197
  /**
188
198
  * 异步检查 WebCodecs 详细编解码器支持
199
+ *
200
+ * 与 streaming-video.ts 解码策略一致:
201
+ * - Android(含 OpenHarmony/ArkWeb):主路径为 prefer-software 软解码,默认配置兜底
202
+ * - 其余环境:主路径为默认配置(硬解码)
203
+ * 逐 codec 探测两种配置的支持度,并按平台主路径判定 SDK 可用性
189
204
  */
190
205
  async function checkWebCodecsDetailed(): Promise<ICapabilityCheckItem> {
191
206
  try {
192
207
  const hasVideoDecoder = typeof (window as any).VideoDecoder !== 'undefined';
193
- const supported = hasVideoDecoder;
194
208
 
195
- if (!supported) {
209
+ if (!hasVideoDecoder) {
196
210
  return {
197
211
  name: 'webcodecs',
198
212
  supported: false,
@@ -202,29 +216,89 @@ async function checkWebCodecsDetailed(): Promise<ICapabilityCheckItem> {
202
216
  };
203
217
  }
204
218
 
219
+ // 平台判定与 streaming-video.ts 保持一致
220
+ const userAgent = (navigator.userAgent || '').toLowerCase();
221
+ const isAndroid = /android/.test(userAgent) || /openharmony|arkweb/.test(userAgent);
222
+
223
+ // 探测配置补全尺寸字段:与业务 isConfigSupported(config) 一致(纯 codec 在部分浏览器被拒)
224
+ const probeW = 2160;
225
+ const probeH = 1920;
226
+
205
227
  // 探测具体编解码器支持
206
- let codecSupport: Record<string, boolean> = {};
228
+ // 注意:WebCodecs 的 codec 字段是纯编解码器字符串(如 avc1.640032),
229
+ // 不能用 video/mp4; codecs="..." 的 MIME 形式(会导致 isConfigSupported 抛异常)
230
+ let codecSupport: Record<string, { default: boolean; software: boolean | null; error?: string }> = {};
207
231
  const testCodecs = [
208
- 'video/mp4; codecs="avc1.42E01E"', // H.264 Baseline
209
- 'video/mp4; codecs="avc1.4D001F"', // H.264 Main
210
- 'video/mp4; codecs="avc1.64001F"', // H.264 High
232
+ 'avc1.42E01E', // H.264 Baseline
233
+ 'avc1.4D001F', // H.264 Main
234
+ 'avc1.64001F', // H.264 High
211
235
  ];
236
+
237
+ // 主路径(平台默认解码配置)与兜底(默认配置)任一支持即判定可用
238
+ let primarySupported = false;
239
+ let fallbackSupported = false;
240
+ // 探测过程是否出现异常(区别于"确实不支持"):异常时降级 warn,避免误杀可用环境
241
+ let probeFailed = false;
212
242
  for (const codec of testCodecs) {
213
243
  try {
214
- const result = await (window as any).VideoDecoder.isConfigSupported({
244
+ // 默认配置(浏览器默认/硬解),业务兜底路径
245
+ const defaultResult = await (window as any).VideoDecoder.isConfigSupported({
215
246
  codec,
247
+ codedWidth: probeW,
248
+ codedHeight: probeH,
216
249
  });
217
- codecSupport[codec] = result.supported;
250
+ const defaultOk = !!defaultResult?.supported;
251
+ if (defaultOk) {
252
+ fallbackSupported = true;
253
+ }
254
+
255
+ // Android 主路径:prefer-software 软解码(与 streaming-video.ts 一致)
256
+ let softwareOk: boolean | null = null;
257
+ if (isAndroid) {
258
+ try {
259
+ const softwareResult = await (window as any).VideoDecoder.isConfigSupported({
260
+ codec,
261
+ codedWidth: probeW,
262
+ codedHeight: probeH,
263
+ hardwareAcceleration: 'prefer-software',
264
+ });
265
+ softwareOk = !!softwareResult?.supported;
266
+ } catch {
267
+ softwareOk = false;
268
+ }
269
+ if (softwareOk) {
270
+ primarySupported = true;
271
+ }
272
+ } else if (defaultOk) {
273
+ primarySupported = true;
274
+ }
275
+
276
+ codecSupport[codec] = { default: defaultOk, software: softwareOk };
218
277
  } catch {
219
- codecSupport[codec] = false;
278
+ // isConfigSupported 抛异常:探测方式受限(如浏览器实现差异),非"确实不支持"
279
+ probeFailed = true;
280
+ codecSupport[codec] = { default: false, software: null, error: 'probe-failed' };
220
281
  }
221
282
  }
222
283
 
284
+ const supported = primarySupported || fallbackSupported;
285
+ // 探测异常且无任何配置支持:降级 warn(运行时业务用真实 config 的 isConfigSupported 为准)
286
+ const severity = probeFailed && !supported ? 'warn' : 'fatal';
223
287
  return {
224
288
  name: 'webcodecs',
225
- supported: true,
226
- severity: 'fatal',
227
- detail: { hasVideoDecoder, codecSupport },
289
+ supported,
290
+ severity,
291
+ reason: supported
292
+ ? undefined
293
+ : severity === 'warn'
294
+ ? 'WebCodecs 探测异常(运行时将以实际视频配置二次确认)'
295
+ : '浏览器不支持 H.264 解码(默认/软解配置均不可用)',
296
+ detail: {
297
+ hasVideoDecoder,
298
+ isAndroid,
299
+ hardwareAcceleration: isAndroid ? 'prefer-software' : 'default',
300
+ codecSupport,
301
+ },
228
302
  };
229
303
  } catch (e) {
230
304
  return {