@xmov/avatar 2.2.0 → 2.3.1

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.
@@ -1,5 +1,5 @@
1
1
  export type CapabilitySeverity = 'fatal' | 'warn';
2
- export type CapabilityName = 'secureContext' | 'webgl2' | 'audio' | 'webcodecs' | 'worker';
2
+ export type CapabilityName = 'secureContext' | 'webgl2' | 'audio' | 'webcodecs' | 'videoDecode' | 'worker';
3
3
  export interface ICapabilityCheckItem {
4
4
  name: CapabilityName;
5
5
  supported: boolean;
@@ -7,6 +7,7 @@ import type { ICapabilityCheckReport } from '../types/capability';
7
7
  * - webgl2:WebGL2 渲染能力
8
8
  * - audio:音频播放能力(PCM 或 MSE 任一可用即可)
9
9
  * - webcodecs:WebCodecs API 存在性
10
+ * - videoDecode:VideoDecoder API 存在性(真实解码探测见异步 checkVideoDecodeDetailed)
10
11
  * - worker:Web Worker 支持
11
12
  *
12
13
  * 音频检查逻辑:SDK 有两种音频播放器(PCM 和 MSE),只需其中一种可用即可
@@ -18,6 +19,10 @@ export declare function checkCapabilitySync(audioOnlyMode?: boolean): ICapabilit
18
19
  /**
19
20
  * 异步检查所有浏览器能力(详细版,用于 init 阶段详细探测)
20
21
  *
21
- * 包含更详细的编解码器探测和 Worker 执行测试
22
+ * 包含真实视频解码探测(videoDecode)和 Worker 执行测试。
23
+ * audioOnlyMode 下跳过画面相关检查(webgl2 / webcodecs / videoDecode)。
24
+ *
25
+ * @param audioOnlyMode 纯音频模式,仅检查安全上下文、音频与 Worker
26
+ * @param hardwareAcceleration 客户传入的解码加速配置,需与生产解码保持一致
22
27
  */
23
- export declare function checkCapability(audioOnlyMode?: boolean): Promise<ICapabilityCheckReport>;
28
+ export declare function checkCapability(audioOnlyMode?: boolean, hardwareAcceleration?: string): Promise<ICapabilityCheckReport>;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * 视频解码能力探测片(H.264 High / level 5.0 / 2160x1920 / 10 帧 / 无音轨)
3
+ *
4
+ * 源视频:
5
+ * https://public-xmov.oss-cn-hangzhou.aliyuncs.com/avatar_sdk_prod/body_ani/M_MX01_show/look/Suyuan_14748_new/1080x1920_fullbody/idle_youling01_cut_000.mp4
6
+ * 源视频实测参数:h264 / High / level 5.0 / coded 2160x1920 / 24fps / 24 帧 / 无音轨
7
+ * 期望 codec 字符串:avc1.640032
8
+ *
9
+ * 生成命令:
10
+ * ffmpeg -y -i "<源视频>" -frames:v 10 -an -c:v libx264 \
11
+ * -profile:v high -level 5.0 -refs 5 -crf 51 -preset veryfast \
12
+ * -pix_fmt yuv420p -movflags +faststart probe_final.mp4
13
+ *
14
+ * 校验命令(期望输出 h264 / High / 50 / 2160 / 1920 / 10):
15
+ * ffprobe -v error -show_entries stream=codec_name,profile,level,coded_width,coded_height,nb_frames \
16
+ * -of default=noprint_wrappers=1 probe_final.mp4
17
+ *
18
+ * 保真说明:
19
+ * - level 与 refs 强制对齐源视频,使 SPS 可观测字段(profile_idc / level_idc / 分辨率 /
20
+ * CABAC / weighted_pred / transform_8x8 / 约束标志位)与生产码流逐位一致,codec 字符串同为 avc1.640032
21
+ * - 仅压缩帧数(24→10)与质量(CRF 51),分辨率保持 2160x1920,
22
+ * 以便真实覆盖 Android 硬解在 2160x1920 / level 5.0 下的能力边界
23
+ * - 体积 10436 B,base64 后约 13.6KB
24
+ *
25
+ * 保鲜约定:源视频编码参数一旦变化(如改为 H.265、调整 level、更换编码器特性),
26
+ * 本探测片会静默失真,需重新执行上述生成命令并同步更新本注释与期望 codec 字符串。
27
+ */
28
+ export declare const VIDEO_PROBE_BASE64: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmov/avatar",
3
- "version": "2.2.0",
3
+ "version": "2.3.1",
4
4
  "description": "魔珐数字人 JS-SDK",
5
5
  "type": "module",
6
6
  "source": "src/index.ts",
package/src/index.ts CHANGED
@@ -521,8 +521,8 @@ export default class XmovAvatar {
521
521
  }
522
522
 
523
523
  protected async initializeSession(params: IInitParams): Promise<ISessionResponse | null | undefined> {
524
- // 浏览器能力检查
525
- const capabilityReport = await checkCapability(this.options.audioOnlyMode);
524
+ // 浏览器能力检查(hardwareAcceleration 需与生产解码配置保持一致)
525
+ const capabilityReport = await checkCapability(this.options.audioOnlyMode, this.options.hardwareAcceleration);
526
526
  if (!capabilityReport.supported) {
527
527
  const fatalItems = capabilityReport.items.filter(i => i.severity === 'fatal' && !i.supported);
528
528
  const errorMsg = `浏览器能力检查失败: ${fatalItems.map(i => i.reason).join('; ')}`;
@@ -5,6 +5,7 @@ export type CapabilityName =
5
5
  | 'webgl2'
6
6
  | 'audio'
7
7
  | 'webcodecs'
8
+ | 'videoDecode'
8
9
  | 'worker';
9
10
 
10
11
  export interface ICapabilityCheckItem {
@@ -1,4 +1,19 @@
1
- import type { ICapabilityCheckItem, ICapabilityCheckReport } from '../types/capability';
1
+ import type { CapabilitySeverity, ICapabilityCheckItem, ICapabilityCheckReport } from '../types/capability';
2
+ import { VIDEO_PROBE_BASE64 } from './video-probe-asset';
3
+
4
+ // ==================== 平台判定 ====================
5
+
6
+ /**
7
+ * 判定是否为 Android 系平台(含 OpenHarmony / ArkWeb)
8
+ *
9
+ * 与 streaming-video.ts 的解码策略保持一致:
10
+ * - Android:主路径为 prefer-software 软解码
11
+ * - 其余环境:默认配置(硬解码)或客户传入配置
12
+ */
13
+ function isAndroidPlatform(): boolean {
14
+ const userAgent = (navigator.userAgent || '').toLowerCase();
15
+ return /android/.test(userAgent) || /openharmony|arkweb/.test(userAgent);
16
+ }
2
17
 
3
18
  // ==================== 同步检查(constructor 使用) ====================
4
19
 
@@ -118,8 +133,7 @@ function checkAudioSystemSync(): ICapabilityCheckItem {
118
133
  */
119
134
  function checkWebCodecsSync(): ICapabilityCheckItem {
120
135
  const hasVideoDecoder = typeof (window as any).VideoDecoder !== 'undefined';
121
- const userAgent = (navigator.userAgent || '').toLowerCase();
122
- const isAndroid = /android/.test(userAgent) || /openharmony|arkweb/.test(userAgent);
136
+ const isAndroid = isAndroidPlatform();
123
137
 
124
138
  return {
125
139
  name: 'webcodecs',
@@ -134,6 +148,23 @@ function checkWebCodecsSync(): ICapabilityCheckItem {
134
148
  };
135
149
  }
136
150
 
151
+ /**
152
+ * 同步检查视频解码能力(仅判定 API 存在性,不探测具体编解码器)
153
+ *
154
+ * 真实解码探测由异步 checkVideoDecodeDetailed 完成(新增能力项 videoDecode)。
155
+ * 此处仅作为 constructor 阶段的快速占位:VideoDecoder API 缺失是确定性缺陷,
156
+ * 任何平台都判 fatal。
157
+ */
158
+ function checkVideoDecodeSync(): ICapabilityCheckItem {
159
+ const hasVideoDecoder = typeof (window as any).VideoDecoder !== 'undefined';
160
+ return {
161
+ name: 'videoDecode',
162
+ supported: hasVideoDecoder,
163
+ severity: 'fatal',
164
+ reason: hasVideoDecoder ? undefined : '浏览器不支持 WebCodecs VideoDecoder API',
165
+ };
166
+ }
167
+
137
168
  /**
138
169
  * 同步检查 Web Worker 构造函数是否存在
139
170
  */
@@ -155,6 +186,7 @@ function checkWorkerSync(): ICapabilityCheckItem {
155
186
  * - webgl2:WebGL2 渲染能力
156
187
  * - audio:音频播放能力(PCM 或 MSE 任一可用即可)
157
188
  * - webcodecs:WebCodecs API 存在性
189
+ * - videoDecode:VideoDecoder API 存在性(真实解码探测见异步 checkVideoDecodeDetailed)
158
190
  * - worker:Web Worker 支持
159
191
  *
160
192
  * 音频检查逻辑:SDK 有两种音频播放器(PCM 和 MSE),只需其中一种可用即可
@@ -177,6 +209,7 @@ export function checkCapabilitySync(audioOnlyMode = false): ICapabilityCheckRepo
177
209
  checkWebGL2Sync(),
178
210
  checkAudioSystemSync(),
179
211
  checkWebCodecsSync(),
212
+ checkVideoDecodeSync(),
180
213
  checkWorkerSync(),
181
214
  );
182
215
  }
@@ -195,12 +228,15 @@ export function checkCapabilitySync(audioOnlyMode = false): ICapabilityCheckRepo
195
228
  // ==================== 异步检查(init 详细探测使用) ====================
196
229
 
197
230
  /**
198
- * 异步检查 WebCodecs 详细编解码器支持
231
+ * 异步检查 WebCodecs 编解码器声明支持度(仅作诊断信息)
199
232
  *
200
233
  * 与 streaming-video.ts 解码策略一致:
201
234
  * - Android(含 OpenHarmony/ArkWeb):主路径为 prefer-software 软解码,默认配置兜底
202
235
  * - 其余环境:主路径为默认配置(硬解码)
203
- * 逐 codec 探测两种配置的支持度,并按平台主路径判定 SDK 可用性
236
+ *
237
+ * 注意:本检查使用不带 description(avcC/SPS-PPS)的 config,与真实解码 config 并不等价,
238
+ * 其 isConfigSupported 结论存在假通过/假失败,因此统一降级为 warn,不再参与 SDK 可用性判定。
239
+ * SDK 可用性由 videoDecode(真实解码探测)负责。
204
240
  */
205
241
  async function checkWebCodecsDetailed(): Promise<ICapabilityCheckItem> {
206
242
  try {
@@ -210,15 +246,14 @@ async function checkWebCodecsDetailed(): Promise<ICapabilityCheckItem> {
210
246
  return {
211
247
  name: 'webcodecs',
212
248
  supported: false,
213
- severity: 'fatal',
249
+ severity: 'warn',
214
250
  reason: '浏览器不支持 WebCodecs VideoDecoder API',
215
251
  detail: { hasVideoDecoder },
216
252
  };
217
253
  }
218
254
 
219
255
  // 平台判定与 streaming-video.ts 保持一致
220
- const userAgent = (navigator.userAgent || '').toLowerCase();
221
- const isAndroid = /android/.test(userAgent) || /openharmony|arkweb/.test(userAgent);
256
+ const isAndroid = isAndroidPlatform();
222
257
 
223
258
  // 探测配置补全尺寸字段:与业务 isConfigSupported(config) 一致(纯 codec 在部分浏览器被拒)
224
259
  const probeW = 2160;
@@ -234,7 +269,8 @@ async function checkWebCodecsDetailed(): Promise<ICapabilityCheckItem> {
234
269
  'avc1.64001F', // H.264 High
235
270
  ];
236
271
 
237
- // 主路径(平台默认解码配置)与兜底(默认配置)任一支持即判定可用
272
+ // Android:仅以 prefer-software 软解码为准(硬解/默认配置不算通过)
273
+ // 其余平台:主路径(默认配置)与兜底配置任一支持即判定可用
238
274
  let primarySupported = false;
239
275
  let fallbackSupported = false;
240
276
  // 探测过程是否出现异常(区别于"确实不支持"):异常时降级 warn,避免误杀可用环境
@@ -281,18 +317,19 @@ async function checkWebCodecsDetailed(): Promise<ICapabilityCheckItem> {
281
317
  }
282
318
  }
283
319
 
284
- const supported = primarySupported || fallbackSupported;
285
- // 探测异常且无任何配置支持:降级 warn(运行时业务用真实 config 的 isConfigSupported 为准)
286
- const severity = probeFailed && !supported ? 'warn' : 'fatal';
320
+ const supported = isAndroid ? primarySupported : (primarySupported || fallbackSupported);
287
321
  return {
288
322
  name: 'webcodecs',
289
323
  supported,
290
- severity,
324
+ // 诊断项:结论仅供参考,不参与 SDK 可用性判定(真实判据见 videoDecode)
325
+ severity: 'warn',
291
326
  reason: supported
292
327
  ? undefined
293
- : severity === 'warn'
294
- ? 'WebCodecs 探测异常(运行时将以实际视频配置二次确认)'
295
- : '浏览器不支持 H.264 解码(默认/软解配置均不可用)',
328
+ : probeFailed
329
+ ? 'WebCodecs 声明支持度探测异常(运行时将以实际视频配置二次确认)'
330
+ : isAndroid
331
+ ? 'Android 不支持软解码(prefer-software 声明不可用)'
332
+ : 'WebCodecs 声明不支持 H.264(默认/软解配置均不可用)',
296
333
  detail: {
297
334
  hasVideoDecoder,
298
335
  isAndroid,
@@ -304,13 +341,277 @@ async function checkWebCodecsDetailed(): Promise<ICapabilityCheckItem> {
304
341
  return {
305
342
  name: 'webcodecs',
306
343
  supported: false,
307
- severity: 'fatal',
308
- reason: 'WebCodecs 探测失败',
344
+ severity: 'warn',
345
+ reason: 'WebCodecs 声明支持度探测失败',
309
346
  detail: e,
310
347
  };
311
348
  }
312
349
  }
313
350
 
351
+ // ==================== 视频解码真实探测 ====================
352
+
353
+ /** 探测超时:需覆盖 Worker 冷启动 + 内联 MP4Box 脚本解析 + 解码 flush */
354
+ const VIDEO_PROBE_TIMEOUT_MS = 2000;
355
+ /** 通过所需的最少输出帧数:排除"只吐出首帧即挂掉"的假活 */
356
+ const VIDEO_PROBE_MIN_FRAMES = 2;
357
+ /** 探测片帧数(10 帧),解码区间为 [0, VIDEO_PROBE_LAST_FRAME_INDEX] */
358
+ const VIDEO_PROBE_LAST_FRAME_INDEX = 9;
359
+
360
+ interface IVideoDecodeProbeResult {
361
+ supported: boolean;
362
+ reason?: string;
363
+ /** 实际输出的帧数 */
364
+ frames: number;
365
+ /** 探测耗时(毫秒) */
366
+ duration: number;
367
+ /** Worker 回报的实际探测 config 中的 codec 字符串 */
368
+ codec?: string;
369
+ width?: number;
370
+ height?: number;
371
+ detail?: unknown;
372
+ /** Worker 回报的主配置声明支持情况(Android 主配置为 prefer-software 软解码) */
373
+ primaryConfig?: { hardwareAcceleration?: string; supported: boolean };
374
+ }
375
+
376
+ /** base64 解码为 ArrayBuffer(探测片以内联 base64 交付,避免引入网络依赖) */
377
+ function base64ToArrayBuffer(base64: string): ArrayBuffer {
378
+ const binary = atob(base64);
379
+ const bytes = new Uint8Array(binary.length);
380
+ for (let i = 0; i < binary.length; i += 1) {
381
+ bytes[i] = binary.charCodeAt(i);
382
+ }
383
+ return bytes.buffer;
384
+ }
385
+
386
+ /**
387
+ * 复用 streaming-video 的 Worker 执行一次真实解码
388
+ *
389
+ * 判定以"输出帧数"为主、done 为辅:
390
+ * - worker 在区间内扫不到关键帧时会零帧直接 done,仅判 done 会假通过
391
+ * - worker 内 flush() 的 catch 为空实现,flush 失败表现为"既无 done 也无 error",靠超时兜底
392
+ */
393
+ function probeVideoDecode(
394
+ workerURL: string,
395
+ hardwareAcceleration: string,
396
+ ): Promise<IVideoDecodeProbeResult> {
397
+ return new Promise((resolve) => {
398
+ let worker: Worker | null = null;
399
+ let settled = false;
400
+ let timeout: ReturnType<typeof setTimeout> | null = null;
401
+ let frames = 0;
402
+ let codec: string | undefined;
403
+ let primaryConfig: { hardwareAcceleration?: string; supported: boolean } | undefined;
404
+ let width: number | undefined;
405
+ let height: number | undefined;
406
+ const startAt = Date.now();
407
+
408
+ const elapsed = () => Date.now() - startAt;
409
+
410
+ const finish = (result: IVideoDecodeProbeResult) => {
411
+ if (settled) return;
412
+ settled = true;
413
+ if (timeout !== null) clearTimeout(timeout);
414
+ worker?.terminate();
415
+ resolve({ ...result, primaryConfig });
416
+ };
417
+
418
+ try {
419
+ worker = new Worker(workerURL);
420
+ } catch (error) {
421
+ finish({
422
+ supported: false,
423
+ reason: '视频解码探测 Worker 创建失败',
424
+ frames,
425
+ duration: elapsed(),
426
+ detail: error,
427
+ });
428
+ return;
429
+ }
430
+
431
+ worker.onerror = (event) => {
432
+ finish({
433
+ supported: false,
434
+ reason: '视频解码探测 Worker 运行时错误',
435
+ frames,
436
+ duration: elapsed(),
437
+ detail: event,
438
+ });
439
+ };
440
+
441
+ worker.onmessage = (event: MessageEvent) => {
442
+ const data = event.data ?? {};
443
+
444
+ if (data.type === 'frame') {
445
+ frames += 1;
446
+ if (width == null || height == null) {
447
+ width = data.frame?.codedWidth ?? data.frame?.displayWidth;
448
+ height = data.frame?.codedHeight ?? data.frame?.displayHeight;
449
+ }
450
+ // 探测只需判定"能否出帧",帧数据不参与渲染,必须显式释放
451
+ data.frame?.close?.();
452
+ if (frames >= VIDEO_PROBE_MIN_FRAMES) {
453
+ finish({ supported: true, frames, duration: elapsed(), codec, width, height });
454
+ }
455
+ return;
456
+ }
457
+
458
+ if (data.type === 'configSupport') {
459
+ // 用于回报"实际探测用的 codec 字符串",便于与探测片期望值比对(保鲜排查)
460
+ codec = data.configSupport?.config?.codec ?? codec;
461
+ // 只取首条:Worker 先回报主配置(Android 为 prefer-software),其后才是 defaultConfig 兜底配置
462
+ if (primaryConfig === undefined) {
463
+ primaryConfig = {
464
+ hardwareAcceleration: data.configSupport?.config?.hardwareAcceleration,
465
+ supported: !!data.configSupport?.configSupport?.supported,
466
+ };
467
+ }
468
+ return;
469
+ }
470
+
471
+ if (data.type === 'error') {
472
+ finish({
473
+ supported: false,
474
+ reason: data.error || '视频解码失败',
475
+ frames,
476
+ duration: elapsed(),
477
+ codec,
478
+ detail: data,
479
+ });
480
+ return;
481
+ }
482
+
483
+ if (data.type === 'done' && frames < VIDEO_PROBE_MIN_FRAMES) {
484
+ // done 仅作辅助信号:正常情况下帧数达标时已提前返回
485
+ finish({
486
+ supported: false,
487
+ reason: `视频解码未输出足够帧(${frames}/${VIDEO_PROBE_MIN_FRAMES})`,
488
+ frames,
489
+ duration: elapsed(),
490
+ codec,
491
+ detail: { done: true },
492
+ });
493
+ }
494
+ };
495
+
496
+ let probeData: ArrayBuffer;
497
+ try {
498
+ probeData = base64ToArrayBuffer(VIDEO_PROBE_BASE64);
499
+ } catch (error) {
500
+ finish({
501
+ supported: false,
502
+ reason: '视频解码探测片解析失败',
503
+ frames,
504
+ duration: elapsed(),
505
+ detail: error,
506
+ });
507
+ return;
508
+ }
509
+
510
+ worker.postMessage(
511
+ {
512
+ type: 'decode',
513
+ file: { data: probeData },
514
+ start: 0,
515
+ end: VIDEO_PROBE_LAST_FRAME_INDEX,
516
+ hardwareAcceleration,
517
+ },
518
+ [probeData],
519
+ );
520
+
521
+ timeout = setTimeout(() => {
522
+ finish({
523
+ supported: false,
524
+ reason: `视频解码探测超时(${VIDEO_PROBE_TIMEOUT_MS}ms)`,
525
+ frames,
526
+ duration: elapsed(),
527
+ codec,
528
+ });
529
+ }, VIDEO_PROBE_TIMEOUT_MS);
530
+ });
531
+ }
532
+
533
+ /**
534
+ * 异步检查视频解码真实能力(使用内置最小探测片执行一次完整解码)
535
+ *
536
+ * 与 streaming-video.ts 走同一条解码链路(同一 Worker、同一份 avcC description 提取逻辑、
537
+ * 同一套 Android prefer-software 规则),因此探测结论等价于生产结论。
538
+ *
539
+ * severity 策略:
540
+ * - VideoDecoder API 缺失:确定性缺陷,任何平台均 fatal
541
+ * - 解码失败/超时:Android 为 fatal(本次修复的目标平台,故障已确诊);
542
+ * 其余平台缺乏故障证据且 WebCodecs 实现差异较大,降级 warn,避免误杀可用设备
543
+ *
544
+ * 失败后不做任何重试或换配置兜底:生产在 configure() 成功后遇到 decode() 报错即直接失败,
545
+ * 探测若通过换配置而通过会制造假通过。
546
+ *
547
+ * Android 额外强制软解码:即使真实解码出帧,只要主配置(prefer-software)声明不被支持,
548
+ * 也判定为不支持(硬解/default 兜底不算通过),reason 返回"Android 不支持软解码"。
549
+ */
550
+ async function checkVideoDecodeDetailed(hardwareAcceleration: string): Promise<ICapabilityCheckItem> {
551
+ const isAndroid = isAndroidPlatform();
552
+ const acceleration = hardwareAcceleration || 'default';
553
+
554
+ if (typeof (window as any).VideoDecoder === 'undefined') {
555
+ return {
556
+ name: 'videoDecode',
557
+ supported: false,
558
+ severity: 'fatal',
559
+ reason: '浏览器不支持 WebCodecs VideoDecoder API',
560
+ detail: { isAndroid, hardwareAcceleration: isAndroid ? 'prefer-software' : acceleration },
561
+ };
562
+ }
563
+
564
+ const severity: CapabilitySeverity = isAndroid ? 'fatal' : 'warn';
565
+
566
+ let workerURL: string;
567
+ try {
568
+ // 动态引入:streaming-video 模块加载时会执行 URL.createObjectURL,
569
+ // 顶层静态引入会让 import 阶段就产生副作用(影响 node 环境单测)
570
+ workerURL = (await import('../worker/streaming-video')).workerURL;
571
+ } catch (error) {
572
+ return {
573
+ name: 'videoDecode',
574
+ supported: false,
575
+ severity,
576
+ reason: '视频解码探测模块加载失败',
577
+ detail: { isAndroid, hardwareAcceleration: acceleration, error },
578
+ };
579
+ }
580
+
581
+ const result = await probeVideoDecode(workerURL, acceleration);
582
+
583
+ // Android 强制要求软解码:主配置(prefer-software)声明不支持即判定失败。
584
+ // 生产 Worker 在 Android 上仍会用 defaultConfig(硬解)兜底,但校验不接受该兜底结果。
585
+ const primaryConfig = result.primaryConfig;
586
+ const softwareRejected = isAndroid
587
+ && primaryConfig !== undefined
588
+ && (primaryConfig.hardwareAcceleration !== 'prefer-software' || !primaryConfig.supported);
589
+ const supported = result.supported && !softwareRejected;
590
+
591
+ return {
592
+ name: 'videoDecode',
593
+ supported,
594
+ severity,
595
+ reason: supported
596
+ ? undefined
597
+ : softwareRejected
598
+ ? 'Android 不支持软解码(prefer-software 声明不可用)'
599
+ : result.reason,
600
+ detail: {
601
+ isAndroid,
602
+ hardwareAcceleration: isAndroid ? 'prefer-software' : acceleration,
603
+ declaredSoftwareSupported: primaryConfig?.supported,
604
+ probeCodec: result.codec,
605
+ outputFrames: result.frames,
606
+ outputSize: result.width != null && result.height != null
607
+ ? `${result.width}x${result.height}`
608
+ : undefined,
609
+ duration: result.duration,
610
+ cause: result.detail,
611
+ },
612
+ };
613
+ }
614
+
314
615
  /**
315
616
  * 异步检查 Worker 实际执行能力
316
617
  */
@@ -407,9 +708,16 @@ async function checkWorkerDetailed(): Promise<ICapabilityCheckItem> {
407
708
  /**
408
709
  * 异步检查所有浏览器能力(详细版,用于 init 阶段详细探测)
409
710
  *
410
- * 包含更详细的编解码器探测和 Worker 执行测试
711
+ * 包含真实视频解码探测(videoDecode)和 Worker 执行测试。
712
+ * audioOnlyMode 下跳过画面相关检查(webgl2 / webcodecs / videoDecode)。
713
+ *
714
+ * @param audioOnlyMode 纯音频模式,仅检查安全上下文、音频与 Worker
715
+ * @param hardwareAcceleration 客户传入的解码加速配置,需与生产解码保持一致
411
716
  */
412
- export async function checkCapability(audioOnlyMode = false): Promise<ICapabilityCheckReport> {
717
+ export async function checkCapability(
718
+ audioOnlyMode = false,
719
+ hardwareAcceleration = 'default',
720
+ ): Promise<ICapabilityCheckReport> {
413
721
  // 先执行同步检查
414
722
  const syncReport = checkCapabilitySync(audioOnlyMode);
415
723
 
@@ -432,12 +740,13 @@ export async function checkCapability(audioOnlyMode = false): Promise<ICapabilit
432
740
  };
433
741
  }
434
742
 
435
- // 替换 webcodecs worker 为详细版本
743
+ // 替换 webcodecs / videoDecode / worker 为详细版本
436
744
  const detailedItems = await Promise.all([
437
745
  Promise.resolve(syncReport.items.find(i => i.name === 'secureContext')!),
438
746
  Promise.resolve(syncReport.items.find(i => i.name === 'webgl2')!),
439
747
  Promise.resolve(syncReport.items.find(i => i.name === 'audio')!),
440
748
  checkWebCodecsDetailed(),
749
+ checkVideoDecodeDetailed(hardwareAcceleration),
441
750
  checkWorkerDetailed(),
442
751
  ]);
443
752
 
@@ -0,0 +1,99 @@
1
+ /**
2
+ * 视频解码能力探测片(H.264 High / level 5.0 / 2160x1920 / 10 帧 / 无音轨)
3
+ *
4
+ * 源视频:
5
+ * https://public-xmov.oss-cn-hangzhou.aliyuncs.com/avatar_sdk_prod/body_ani/M_MX01_show/look/Suyuan_14748_new/1080x1920_fullbody/idle_youling01_cut_000.mp4
6
+ * 源视频实测参数:h264 / High / level 5.0 / coded 2160x1920 / 24fps / 24 帧 / 无音轨
7
+ * 期望 codec 字符串:avc1.640032
8
+ *
9
+ * 生成命令:
10
+ * ffmpeg -y -i "<源视频>" -frames:v 10 -an -c:v libx264 \
11
+ * -profile:v high -level 5.0 -refs 5 -crf 51 -preset veryfast \
12
+ * -pix_fmt yuv420p -movflags +faststart probe_final.mp4
13
+ *
14
+ * 校验命令(期望输出 h264 / High / 50 / 2160 / 1920 / 10):
15
+ * ffprobe -v error -show_entries stream=codec_name,profile,level,coded_width,coded_height,nb_frames \
16
+ * -of default=noprint_wrappers=1 probe_final.mp4
17
+ *
18
+ * 保真说明:
19
+ * - level 与 refs 强制对齐源视频,使 SPS 可观测字段(profile_idc / level_idc / 分辨率 /
20
+ * CABAC / weighted_pred / transform_8x8 / 约束标志位)与生产码流逐位一致,codec 字符串同为 avc1.640032
21
+ * - 仅压缩帧数(24→10)与质量(CRF 51),分辨率保持 2160x1920,
22
+ * 以便真实覆盖 Android 硬解在 2160x1920 / level 5.0 下的能力边界
23
+ * - 体积 10436 B,base64 后约 13.6KB
24
+ *
25
+ * 保鲜约定:源视频编码参数一旦变化(如改为 H.265、调整 level、更换编码器特性),
26
+ * 本探测片会静默失真,需重新执行上述生成命令并同步更新本注释与期望 codec 字符串。
27
+ */
28
+ export const VIDEO_PROBE_BASE64 = [
29
+ 'AAAAIGZ0eXBpc29tAAACAGlzb21pc28yYXZjMW1wNDEAAAO5bW9vdgAAAGxtdmhkAAAAAAAAAAAAAAAAAAAD6AAAAaEAAQAAAQAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAA',
30
+ 'AuR0cmFrAAAAXHRraGQAAAADAAAAAAAAAAAAAAABAAAAAAAAAaEAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAACHAAAAeAAAAAAAAkZWR0cwAAABxlbHN0AAAAAAAAAAEAAAGhAAAEAAABAAAAAAJcbWRpYQAAACBtZGhk',
31
+ 'AAAAAAAAAAAAAAAAAAAwAAAAFABVxAAAAAAALWhkbHIAAAAAAAAAAHZpZGUAAAAAAAAAAAAAAABWaWRlb0hhbmRsZXIAAAACB21pbmYAAAAUdm1oZAAAAAEAAAAAAAAAAAAAACRkaW5mAAAAHGRyZWYAAAAAAAAAAQAAAAx1cmwgAAAAAQAAAcdzdGJsAAAAx3N0c2QA',
32
+ 'AAAAAAAAAQAAALdhdmMxAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAACHAHgABIAAAASAAAAAAAAAABFUxhdmM2Mi4xMS4xMDAgbGlieDI2NAAAAAAAAAAAAAAAGP//AAAAOmF2Y0MBZAAy/+EAHWdkADKs2YAhwPGmoCAgKAAAAwAIAAADAYB4wYzQAQAGaOl4GXLA/fj4',
33
+ 'AAAAABNjb2xybmNseAABAAEAAQAAAAAUYnRydAAAAAAAAsOgAAAAAAAAABhzdHRzAAAAAAAAAAEAAAAKAAACAAAAABRzdHNzAAAAAAAAAAEAAAABAAAAYGN0dHMAAAAAAAAACgAAAAEAAAQAAAAAAQAACgAAAAABAAAEAAAAAAEAAAAAAAAAAQAAAgAAAAABAAAKAAAA',
34
+ 'AAEAAAQAAAAAAQAAAAAAAAABAAACAAAAAAEAAAQAAAAAHHN0c2MAAAAAAAAAAQAAAAEAAAAKAAAAAQAAADxzdHN6AAAAAAAAAAAAAAAKAAAdhgAAAMQAAACAAAAAfAAAAHwAAAJ1AAAAkAAAAH8AAAB/AAABFgAAABRzdGNvAAAAAAAAAAEAAAPpAAAAYXVkdGEAAABZ',
35
+ 'bWV0YQAAAAAAAAAhaGRscgAAAAAAAAAAbWRpcmFwcGwAAAAAAAAAAAAAAAAsaWxzdAAAACSpdG9vAAAAHGRhdGEAAAABAAAAAExhdmY2Mi4zLjEwMAAAAAhmcmVlAAAk421kYXQAAAKuBgX//6rcRem95tlIt5Ys2CDZI+7veDI2NCAtIGNvcmUgMTY1IHIzMjIzIDA0',
36
+ 'ODBjYjAgLSBILjI2NC9NUEVHLTQgQVZDIGNvZGVjIC0gQ29weWxlZnQgMjAwMy0yMDI1IC0gaHR0cDovL3d3dy52aWRlb2xhbi5vcmcveDI2NC5odG1sIC0gb3B0aW9uczogY2FiYWM9MSByZWY9NSBkZWJsb2NrPTE6MDowIGFuYWx5c2U9MHgzOjB4MTEzIG1lPWhl',
37
+ 'eCBzdWJtZT0yIHBzeT0xIHBzeV9yZD0xLjAwOjAuMDAgbWl4ZWRfcmVmPTAgbWVfcmFuZ2U9MTYgY2hyb21hX21lPTEgdHJlbGxpcz0wIDh4OGRjdD0xIGNxbT0wIGRlYWR6b25lPTIxLDExIGZhc3RfcHNraXA9MSBjaHJvbWFfcXBfb2Zmc2V0PTAgdGhyZWFkcz0y',
38
+ 'NCBsb29rYWhlYWRfdGhyZWFkcz04IHNsaWNlZF90aHJlYWRzPTAgbnI9MCBkZWNpbWF0ZT0xIGludGVybGFjZWQ9MCBibHVyYXlfY29tcGF0PTAgY29uc3RyYWluZWRfaW50cmE9MCBiZnJhbWVzPTMgYl9weXJhbWlkPTIgYl9hZGFwdD0xIGJfYmlhcz0wIGRpcmVj',
39
+ 'dD0xIHdlaWdodGI9MSBvcGVuX2dvcD0wIHdlaWdodHA9MSBrZXlpbnQ9MjUwIGtleWludF9taW49MjQgc2NlbmVjdXQ9NDAgaW50cmFfcmVmcmVzaD0wIHJjX2xvb2thaGVhZD0xMCByYz1jcmYgbWJ0cmVlPTEgY3JmPTUxLjAgcWNvbXA9MC42MCBxcG1pbj0wIHFw',
40
+ 'bWF4PTY5IHFwc3RlcD00IGlwX3JhdGlvPTEuNDAgYXE9MToxLjAwAIAAABrQZYiEACP/HVX/9Jx4AAADAAADAAADAAADAAKaAAADAAADABxIAAADAAADAgYAAAMAAAMAQbAAAAMAAApAAAADAAADAAADAAADAAADAAADAAADAAADAAADAAADAAADAAADAAADAAADAAAD',
41
+ 'AAADAAADAAADAAADAAADAAADAAADAAADAAADAAADAAADAAADAAADAAADAABkEOXuPain0AAAAwADGOAAAnCVwB1gdgBRBxsFDAAJAA7Qgk1QAGJjj41H24u2v8L16bzd9WExiOcxt+AAACJuiPKnTkt78W9x4i/HpVtT4CHi0/B5P9UbeT6Ch3AAACy1IAfB6jJ4OCUy',
42
+ 'vWkkgY/9Pd3lpe+hgFs/QHKAAEBUWPNT0PT7mqhPbBxPneArf6l9nliw3C1hP9VHPSLIkpWKAAADAAADAUuWqxALGzu0x4wENnFvB3t7P2SgAABEs/f207h8TR9hmOTgJwdWW3G3KatCQ9OKr2AABFeZoAPm9/U2M1dgVHD1FF2ZLwAABT2bNKSNBIFgw6sgqSj/POdR',
43
+ 'GDLAS5cGEmvjIJofyPoAAq9DaiAFmRSe+IV0AYKa53ZjdWLpKjHPAAB77BxY3rbtRM+3PspjmQptJfGNMlT5zAAAdHQBg3JsUaqvPYJjTvAl2UNE+AzZnI0FchAACc462IcigQCR4n588pQvKSyxIjKmhgAAKScBMTqA/vaR9AmI960pgXsjMif0im3QQrZb4AFgkekG',
44
+ 'SL7sXSNGpOX5i1s4AAADAPM4J9FjROm3ojTv93j45exr6P8McNiE2Wub48FOuj7PGGgABJ4Q5/vSjqKp7T8hQkUXxuF92M6AAABkQW/6EYT4W8E2EYnseyzTyRluE+GWcO40gks7Qo85SAlsxccwlBSncvI23YphxaSPTpTcLSTHOFzs4FngqucCjc2du4fjHZjvdPkf',
45
+ 'A8/AAB7vGWAal52iRxkJv1WI+T7QPgdfP4UKs2bODUQUllX267IAAAMAAoEgTp8hTTPkX48zokAEI1xzu5WWnq/pKO8HKfagrb1y87zfMgACbpL9k9/xEKKGwasRDhLRRFSAABe5bhCWksSXC6ybByEROkYfxaEjlnVUyj22ABMcKYAAAG9DIXUBzyBJBZ2cjvMBi82G',
46
+ '7fe8rg7eAH4o2QutgB8fibV9+M7KdqMG38MJjYOnvEqO9ZqQfAAAEZ9pF7o3U7yOB7TBzcTl0a4THE/9QDUfagFSToXQwgEbUaiBm+HHiwAAibulH4B2S/F0TwMzwGPT88gn0xPamCxaUbJ3a83C3R/de6UKzd/hNTtcETMsjyo0fIYfR0I/tDx5MmWq2JLXzetFB0Qi',
47
+ '8egJ/33RIth2bpx/yDbQAA0ju5HFEfgmJ2vXU7Nj7lVE7GE6qBDmpVl9sThGHyzfUHYACLuzUkdYpokOeiqsYOTBvPMhXUMJB9Lgw7LXLhkEGJH4QY5wcqAAAASAz8Kwvkv75GR3OQcG14zOFM6vLCxUc/FuGN+nNMxbiPOYyY6SO1gBEL4o6b+igz+6Zq52a9FINl+8',
48
+ '8KfM8Pe0gwHi4P9dBoCYlOoKJ6n6uikxM+5lf1mvgjgdnlpcrae74z0jZQ94TrV69I371UOoFqRktFHRTdAAEiv8jpMAzdPh39MKGoN4bOTndd5Hr/B9QAoob6svdC8wPElCer0m+QQmYADAlry3suadFguRKHNz4/nk/AuSezsZ5EntL4+pl8Qf5FusbKf+NAk7pv/6',
49
+ 'tKuK/edu4XUAAGCGjjjNk7pwX7gpZ2j8Gblg3hW1G8NrGxod+kuAAAhF9/cab1rG0Gxsrwtay1k3AkPyz5eoA875OKffOBw2YlLAAL019ghZ3WXpUU8Irt70/6bXzgEYd3jOjhkaWECMaDN3wc+krxGXAsvAKNanKCvOCpGCMfby/A1x6CUZ1jrLSKlakAAIZ4aCpLPI',
50
+ 'BbM+XOQLVso7wFO+QAK2xqO27z8VW/8aN6tmK4XKq6qhch8mvLO99RH3VPrcDuHBJ/ikxOssjS8f/ZYHTTZDxY+NgSEp46AAAWKEpcPzJ+mf3WrElkVCPvDZ6oAAawUG1Wfa4+ftGkc299KaOkEdvWgRlAGrGUmQKuDCY9Df5u6bBfgABD28kDZRVXvPQQNTzCdMPwrA',
51
+ 'rVQwmVD5v5/OPeaQd5rBupmDrMBk4f2YKIKMMmZUs9DgCmBp3ypejO22QkMREpQkwAAHJR+5LPkfHTjRtERikfI5KkAWVMKCDHm60OpYv0MO9kDHBSl0fIGR6FP6PZNQn1OTKSuxkwAOKIRcInPLYeAziAWlkQfG30Azl0tLvkY530imR/3zZLgL/auuI6xivm02jZw9',
52
+ 'HMxagAkYY2r9sEcgAQkuAa5aV1IZtOasC6HNdAcn7yD1+sCag1aqOMBxb4ftI9GkrPIxBqO4QyatWXmnUIAAfAwMY3QLD9xOyNsH/gPKPb2GN4bb+HOGiyQ5IoUDN0XfLcQoN4ts5AB6wRwCufuaYBuf7KScvdThTABYsWmWg6MIoLhfOPWC0iDn2WqABmEjZgApOsdt',
53
+ 'SxkbgN0L8wSjCYNWg0L3HQYYAASldcHRBvmS95//m6y6idyJ+EjGneRsKlPMVMnNyyEA2xJRFdIvCgAAVRV9xOHpKzbhiyEaxSLA7UErEeAFAD5QVYayP47Vmkpy0X10kaeZ/VL/PBimNL9i2GnlRDEFAE6gWLnXSkUdNEN+44++N3IKaHUWXOeR8KZ0IAsOKh2erntR',
54
+ 'e1GiOBv7bb4GpXSUfYlEESDYG93j5dQb6DBRYGPjqUMZytBAdL68qPNG0/dMxNSSmUC0dAgGGARCiFyrdUjWLikujLzsUE+TqBbuZ54jlG4amIcPStPy1nw5AADT1Sjq/no95ZGjszXjhGNclUM1eJvLYAOQzfMjFXtDv3H5mkeiS5zE565LzH9dvjiLO+eBoVGtPznU',
55
+ 'UAwtXP/UPxP3w/4+AEX4nQ5qH3uSjdhic71oXogEji69DwotyO/5/woZF3yonfFgBNlm0ueG9N3eCHDeW3N/d4Ru7NDA5KvqVbvZKmFG3j/lIw/gYKmm4RGMcR7LX1V08B+Pq4ACTGN1er7/wIoHogrDkQEIy8vvT7hEkOyedFGLAeiYdI1VtqVm4cevbmyCANQ4AAnu',
56
+ '64Nw5kUJ7ra/lmmA65WpsPXqaY6ejJ9qwqU67GUq2lekapQqRG7vUrd/VhSavytuZoG5EgAACuTCVlqr0YrjWWdChZnPCeAc6FnDdorJbsmI7qzuPrt4+AoAFMSIBwSg/NUC579QZNa5t1A6sOOU43E8peL6YodhifylgbhvTvzUFz+xJkk9hPAAAXpAoUIwuR991FMQ',
57
+ 'LcBpxQt9EQbN8NYixYNO6kM6zAD5DU9vH2OiBU2CEMaWKKVgCgk7LyB6/4jiSqrSmMMkMzmtr2Ku8kearYX4wh2p5OYYAAKqhVI+NHKeeSnHE7kn7eSmfy5zByV+f5iXX/hTM0Yst5YcuTatzS65vxw/nTaVlFAk28lbUSYQGXXmQ9eC9OfKJpG9//fDdo8I8eWtLU71',
58
+ 'tyvprE1iHazeOk0lrDJgAAAZwH2MkGICOKptWDE/t9pciEN+8GL8YZsfZK7cyUDkqp+vLljDCWrgEomORu1KREe9fY/hYki9ix/voLyyi7Zj6UOKCIxeHZggkTZqfx6UP1e2a23ZVXGAAACdDKGn4J1wlZVewql/WWeHbNApHXsa7dAx6C5WgAl4HjMpAAmlfJRyOevE',
59
+ 'wVf0oZjpHxd5WM5P9m//by3bfphoZ6TXI0Vw33EU/HExV+qgx9YAABi5ERRDSeHcZkXPh9CvYfb7HvZ6wg/93XG7dDECTtYk661jUUhoGP1LY08KyNhSTwISSkLODdDD381QlKT2kvXOhMZsCprC8LBv3wDv1GMwg3ZwAAAFQSCh8jlPrXAJmDF4HIlxHUjuxkgkJdei',
60
+ 'cS1iabx/xSAQophqjhEK/cW6L4Z7KrVSRuTOCTdm3GXnpH876L2ctr9+hn1L4cs5XfvlhMgAAFTc9PmKLeJhws4y+CZFHr770jYObO5szKiNaQtbYA2Z1ADVlT1gdh7KS/19fyfkhstiIFD3CXyJCFpGQeuC79i5/KSUienXQ3z1fTtVJCO1YrgAeSiCdg+rLLpHhkHn',
61
+ 'g2gkRry8T91t87lHklCkPXl/JW7wwW3kxrjOk4GORcukFlg/wKtwF9TJZubscnmjUFt5L+UO3QRDw2+RQO2jcPtg7WJaexDb+bKLpsSuyAABLGxLzlBupAGJuyXq9RQGJ75yG/EcSzDCTNvaGS03qKa1F3VwDDKI6Alnqi9+0uAAYnGG+0E6UC+FniWah6s3tGi8Kyj1',
62
+ 'wL50anznYbpIicpKEIh7VJud5231ZyVyTJKCCoEAAY5f2m7QxQd+o8NElsgg8F6kBtSRgeV/MSJYWF2o1SGTIfrHSNygAAeNupbd0vcywt0zMqSkiIDuAY9JybhMCw+rl9tfHAKQ63x9mEtywoKs3hh5gAO35wyIdXkD7JDV/CWKMSFwP9RpIGOkggSP+QgCVXh/Sedj',
63
+ 'wJ9a9qAAr8ZmE10M2aHjjLyFJTQS7geuFHnj8ZD3wL6Yl24fH9f29qJQ9hNVhKyuaXggAAKqB9h791oBa8AxtsB8qjZgUFRjR49vt+eIcgBBozt7xwUCgUAAej+cp/reCIR1Fa19qFlgUM7UQGURldAASkIbfzAk6BpCg7G8zGjoPghwKnlWKWb3jHJWBrIcDWRj4Egl',
64
+ 'CgCe2HwYZ4WC/TtaargTsEf8nvvHsg1h5cEIlJmMwItooRktPCo6uxcVQ2OG71PxbIAABFzJ6ld3r2sn59LfZcaSQNMQ/34p2A6uAbANgilIiv9zkZKvvd4R9DgAxiCPQXy4QTXjEkYwlIScuDlqZ264XR8XuA0kaYzvdrAN5zOqqTFyAGzJgAe0eKCdoddnZ0g0AAAn',
65
+ '69CDaOZ1ksreJ2QP0gciKe2w++YGDb5iBRWp+EVxaAcZ64PBbwUgLyNl7WeS2LnCDzd0EedwOJwrYRx7fZ3a1HZNbzBtLtBWKU4EwWAACcoouqPbYNFH61K4grSUGi86W/42NsCAXOayOJVWrjPbw0WAA9UQcR9iLFE5RJRzJ2CqSEcUsk+5pQv5LkWcIuB4oc+7mnwp',
66
+ 'JV8F7ffg/NYgAIPB7jlSGtNVPqgGhnN0WmDqpAy5eQLCCPl0ZWMeTxwCtkSb82WAAkM+cDV59zTTSFLC0aVeSqi1qO+YvK/kNBYdFwFe8+KeTIQAXxXarEkWADMUy4yeN7IQJbibWnGtqm6B+mFIFKsgsGFdT7Te6aUK9UZFEb3Do53sAAQrEYcmmTBcvWbPbeX+96Gq',
67
+ 'cRnlT/WeuezL/e8g0LTZbSy8VmYEVHtAADRYGcjVAsVDPfqpJX5pxPzcwGBsXyiVQe0JuBT+sMvCwQmWvXZuwBCbKurXo0sdUUexp7fNd5ZrTcojj2LkqgNhSrPrei6odRJS/jQAAD82aVmqD4/tEdFkbismt77uF58PCh3NhTKCBe6z9hbHdXLWpjoZFD83K6QHj5r2',
68
+ '1Iu3gSpAAAjvTvaSBaFzSmSjNRZK0x8WNuZF1hfS6rIfcx2l+g/LEsvAAACq/QPvNe1o9CasI+JZqYQxEIpXxoS78WIZaVNB+UTNo4CUC9pmmmIUoZ6ap8M8YlgroAQIME+AAAtunynbGeLJmZDHXRPJxAvv9HhdNoAKnAVpeLCA2PHpNk+istp+xjyIQiWhV5oATi4X',
69
+ 'cnSyBXYX6I8O1aT4CfwAfugtyAGYvY4g2aVdzeCU7l/17AAEzdKoq0U74eT1mto+xMzrV5AG9wm8TbZxKOmzh2hO0YSYcFKQ+nRAjzePV8IABkfE4Eo/rnJ7Td5W/qs9ILfsinAL1MWXiOAXLBAyGBeETZ0hJWt/HtyWVUWAABs/g6q4lK1LJ0iIMLr0nA9i/AESH8XT',
70
+ '1+7jEcAdTNcWaScL8u37enZBAFSP9rKlRX2brKAAUuNwu+oiTJBASqVVAd1EpO1Aho5bgAW1n/xpQdTxFVU3ulFnT/wWXpYzqstiQztQABOXTaO9Eciin9J0YVSBrDckxjdby0NZrfqNegABSpQaSasAxfI1l3GJ/i5M0rJLPXRI74oTKakD6WbfMGDtSP5qzf9V7vFA',
71
+ 'Y1UKDyC+NRPdd3S0b+rhf87dbAAASpMWkrjpajxSXuOsyCBgJ2kLhnA3Xa2G3Lue2AG3R4NgSTZRgCR5LTVLVqPyscnkwXDQkAEgASN6DP2R80rN0mvi8V/D1AcP1ePS4uAGTfHrwB9n9JjltiYLZP1yCk/7nVEkDmAd454g2M8xPscYU3py/HSetBe7X7evW9f+4gPC',
72
+ '9e4LUABZbrGzGcUvzotdb832Uzu0Q1xytj7nWJdY7L0KJOXVaABqUr2qG2th43i9OUECJy8ONoBpVmGa920C/AA7PH4am1DvXVvyLo3ix5/atW+FdwAAcbZ5/hxMAU+3cYwB1bY1vk6RZTQnLNzKHQABuiP1JbfOy9NFxdyEPqdjox9W5ktALJfcOcAQeEoXfmzs9N4+',
73
+ 'TH+OLaIlr282zEGvoDBQaYcnuNYAAOyW/TstWHU5UyDs9raILV0FhjrAK+Rruqrjj2bTtFfTSyo0LUHsDm5HlKp4DXzG/9/1DRcNRAE1Wxl1dTa6HAAY5Yv4oeqf03tIH1GpXVlRgRBExnd7iADTLh3vVMU91xVYmhdy4xQFaki3Fk6Zdtbk38v8kdqAmOvGvS4wAAfW',
74
+ '0eXvxxYFDGF0WS2Riv6/JsKF0UnMq467S5y8rGczMwACIriauafKIwVTZ6Oi8f22uv5x5Tl3SE72Kl1xKAYTU+itbCIN8xTZhJdZf0rTuLo6uLkCpkrXNEknimnQI0m/wACfbJs6xneGaEmad7RjmA2AZ8DPVWY4o/6G3QAAAwHbyKgKPaSE3laBe86egCcCJvPj10LS',
75
+ 'oaXfVzRnVUvBJn0N5AdqyK1gA7ecpdAXKzyhwd7azcXJQQQTADWJggTjPFT9wD7oIb8Xo0sGkrI9k6lSuDUKgB2KIVBMMwYecMD1JJ24JTmIVzuOjABjFLPS5JWrwpv4pnUlgXLtL25pDg8WAAL9QT+ba5hqXlx3qDzNw75ffP4hz94B9/3aJ7whvhjM6bv/mOziv//+',
76
+ 'G1+aAkn0qZwAA2hGrpR9TYtSFPi+krphtyWMQAAE/z7Szty1w8wuX6f2mSjvC6Mip/MCG6HzzOTm9bSGzC3QvlwRtQMADZNhtMHuqfxNJ8lpEDdzKWNO8hRb/4nVRLYAAIeRcUP1Pza39r7snbxMRFAZAVp9KdqADiGw9MF1moq4Tns3lkf+3i1ddREGFLhfQT+asAAE',
77
+ 'HbOaAmamxLHLfIxfAMhBqMvvmnziZAAlLyptJkG6v0KLbmMeiIs6rrpdjUGHTKgkzvRMG8NNZFJRp69d69mudUQd/GEQs4ADkNoKQOn3/9zt744FijS0Z17uz2guy7wABInf9ERhsdKQV0FJvkKNdMryzbKwK5mnakAtzNitRJa/9upagVo9HkuKQmHE54EZWLUBYGQA',
78
+ 'BQPiMhOZOhtobNy7e/bJOP8J/mW54hWSzHKXAAIqIFI/VCYpAB75PRYWVewfLQtPXwvj+6UhuZQMcyDI5bS36a3ICOZnBWk9j8IJaUzljK2Dt3fAANsmr6YWnclVKK5o1j+nRLxGDpBAY2SZPtwXF+LpJ0oAABUlWPoxC2dpTVYSvFSTGXUAIAg6sHJOjLnrmAlxS1eP',
79
+ 'xdcaOD4pC0R9lltDuOeQSQpZx7YAAAMBwhQoCki/3JMK+AVEtuCcIp0tVUI13CQJRGrxmK5eVgAB4T9fWchKHk+XEgUZkOu7ef9ZE8UVxM4nzmLuKUACaGgA5QVcGVq3EnoLKzFILaNa2hRJ8e72egAAEjT49Nl8bg7QItJWA3ylklVBlZiVqamCoQGVoQAAGwL6qbrm',
80
+ 'ymv1mdN6UmoTtCYVYn1nWnWIB+gx9AAAAwJqXWE2NZnHtxHTxN6qmfvgsxV6X5V3lxVmFlJLKGsSfPvgAAGl4K/ak6NpVXN3TOg7kwdAS/pQ2+pYAAADAo6ZzhmltQYbFBR2k25g6N/e0bInvsVRWgoYGgZhK7wBcnn/aafImfm0l+3iI5hbJuRopkeJiMLRrgAAAwBo',
81
+ 'hmqkVx0KNOb4qVZSHmEqDYpI3H6z0Zgrl9ykjXi9tdYgAAAakZWBUlk7QCaV8znUeVMXYmA4j6QAACXpbb5PxTbkpkFIVqz9XI9U+W2YEPLctPRsAXBgAAXO9wTAYAxi1E9joczpYplbAAAGgzTAFovrYu2XTtWfPay9f3ChgNpHTbqGF0W9eIcQqQEHgBBrJ3c1oxcy',
82
+ 'ZHiZ99s37xMueRukd7AAABAP25WReHwv2YUOOkVSUEyG+Vi2lSQrQnFI8tAD/JAAAFZSGzDixx/k81yCC/p12uiQ5dqxcY3OcwXtN0a5gAABBxbZHPSZPMM25fBXNonhEF+H61l1fTv+zh/yeAUCaDE4MmM/U3KJWtQ+gsQTdI4TBgAAAwEEJxYz6NhkqZPBrEIYqXgi',
83
+ 'Qgn82rqA4MAAAAmlw7m8NGqWGaa3rsfgRaGYS/Q022vRgyVSmfzH5wAVVDlwxUmJSW0KJ7LJlvYARyZZOi3gQjE363bsBYql2V/604y5QHkkyXbAfMLqIm3cr6J6gcvx9T2uf/cAAAMAAAMAoLTggjJCl6ZfP+GSCCLMESY06TFLs1Ocq9n/9HS9XjyUGrIjqiVcDTy7',
84
+ '+fVDMVrEtgKO/HfAAACSrLxGaaTGqL0vmQkOG3H/Ecu3y2Kd8CR+A37rOM2jO2L5rFsAAAMAAAMBODYHof2+xChTk2F1T95hlGsKrhfF5gFDiA+P0pYsFYhLhMxyIwACXcstK+FFdo3+4YJc/MaI0R43OfmAnbUc2dRX/dwAOYYui/tptTcafY8B10kjWBoAAAMA6hx8',
85
+ '/8pkpHguZnOIb/CGJuS5QBLTOMG0ZGMf9ZJPQDsogAGLVecrjuOxZ4zFBmzarqvTs5q0xDigunmUkBo3nuMFTXGR+cdVkM+M2AAAAwGLPwktiudEcFI6PSeBs8Z2kpvY277jVAAAAwH/uoOBn/n++0usA3Zxuc416ugy4NHoHm72rShRkuESu3aAAAADAAiuk9Q6FbVo',
86
+ 'AAFl/pyEboOvAAAAwEGaJGxCPwAAAwAAAwAAAwAAAwAAAwAAC1V0Dm3MAAADAAGKVPoDdcEwImbhwAAIincfaIP5JBZqnQ035V7HU70SvyUQbDOaqwAFEGgTbHFrB4If711OAAADACs5nS7Q+a9yV7E4h0Ez1uBP3EfsAkhQH0/6NXQtgACBym+P6AAKGKRlAAGhLAPY',
87
+ 'bXgwjYYLPdAAADZY0AH6akAAAAMAAAMAAAMAAAMAAAMAAL3zUpNAAA5q0b2AAAADAAADAABXwAAAAHxBnkJ4jf8AAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAaH9F8l9fAAAD',
88
+ 'AAADAAADAAADAAADAAADAAADAAADAAADAAADAAADABZRAAAAeAGeYXRF/wAAAwAAAwAAAwAAAwAAAwAAAwAAAwAAAwAAAwAAAwAAAwAAAwAAAwAAAwAAAwAAAwAAAwAAAwAAAwAAAwAAAwAAAwAAAwAAAwAAAwAAAwAAAwAAAwAAAwAAAwAAAwAAAwAAAwAAAwAAAwAA',
89
+ 'AwAAAwDpgAAAAHgBnmNqRf8AAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMA6YEAAAJxQZpoXpaJgI3/AAADAAAD',
90
+ 'AAADAAADAAADAEK7uW0HwlxaLod+8n9y2dE25CO0wjUkJGVbDJkwS8LCJUO7zdUKaJbTaq9M7+8ELtuVY1lZ/btIu3li6IjY+Ov66jvRBrqSFDjfbyhKleZR+9UlVZZpse5gdDl8MHUwwfzGYregw2423ZUw7Sn97g8RMf7SHQTXwRF7j/fkwGYUSkpmG9qWg1wS5TUK',
91
+ 'VFk5M0SKYRgM8CySrhljPGdo9wvkw7uTbVjt2sKEXEIW8MI2esm0VrD+S7cJXtCVWSMO8IXxanrG5COpq6ZBzvfJ6ymDX6iDoAobTICelsgb2WAIHUtlm1f3rGKuyef0Yf94xekDEmlqqRdsZCYl/hAPZll1awBIKvHqkOOaNHoUQ62/emowbfJvP8fe2zjtdnDey1cs',
92
+ 'pjpqOFSbjASkUO83ddc9652op2XltZTWa56Wr2cwUBxIx4W+ad6vdIGrwb1hZS1r65BGAzj+/F6uKOyG4RGC0lLE3NetVXhrogaPBoQwSdt89QFY3qGVAIgFqvucRcWLey4lhk557q2MjSz9jcPjGiw9PzwRTa30dLDkktwSEI/ZwQCZfi/acZFD8yunv14AYtypuBoY',
93
+ 'vVceexnkeMEiRBFm0DIKp82mnZe1zehd/WlQSVJSqovoFvqZHgb07LtiLneXBF53QZEMxH0jfBCj/H90s/rz1CVEvjKAc/kdkjmmhr3ASGRcfBtqVn44UEkIzM/fO6Chqxv7ZW+Ig1jla7pjCRmOE12qPIXGW74zBcIRSpdqo+3cseFFrvQFFcPwAAADAAADAAADAAAD',
94
+ 'AAADAAADAAAl4QAAAIxBnoZuUTG/AAADAAADAAADAAADAAADAAAdTB+gAAADAAAGx/ThJHkg+TwAAAMAAAMAAAMABuNmEeAGAA4H75AAwkuAAAAHMY3sAtntw3YegAAAAwAAAwAAAwBbO3gyAzAAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMA',
95
+ 'AAMCTwAAAHsBnqVpEX8AAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAAMAAhyDSQqCJgAAAwAAAwAAAwAAAwAAAwAAAwAAAwAAAwAAAwAAAwAAAwAAAwAAAwAAAwAAAwAAAwAAAwAAAwAAD/EAAAB7AZ6nbkX/AAADAAADAAAD',
96
+ 'AAADAAADAAADAAADAAADAAADAAADAAADAAADAAADAAADAAADAAADAAADAEO2/cAPCTpcAAADAAADAAADAAADAAADAAADAAADAAADAAADAAADAAADAAADAAADAAADAAADAAADAAADAAADAAG9AAABEkGaqUmltSYAi/8AAAMAAAMAAAMAAAMAAAMAU1eb25GTW/axz5g6',
97
+ 'EYyReAAAK0G/1DwhHeAA5o1vLjsc2UIAFfG3Z3xdmYOiZZV+6V+frSCRE4N7MdfTkM8mKNUaQ7EmcIWLwnNbMLbV6AFLHIFO35ifNoCOrdZoAB1drW6Gb1R/Z/IBXyR0svRZbRI3eud6M2QYuh05mAR8pwtlg09krtIl6gBwCymZNCRhJGaUUWgQYKTJ0LXNGiHgHsdp',
98
+ 'vA4X3aZpTFo2nCabF49whgE0JXAAAAMAAAMAAB490CVcyvQos44EACAVEU28wX7/HBFRAAeQWxupPrMDRdBQAVFFMctkYAAAAwAAAwAAAwAAAwAABBw=',
99
+ ].join('');