@spatialwalk/avatarkit 1.0.0-beta.68 → 1.0.0-beta.69
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/CHANGELOG.md +24 -11
- package/README.md +102 -18
- package/dist/{StreamingAudioPlayer-DrTBMLSq.js → StreamingAudioPlayer-DiIRp5nx.js} +109 -1
- package/dist/animation/AnimationWebSocketClient.d.ts +26 -0
- package/dist/animation/utils/eventEmitter.d.ts +3 -0
- package/dist/animation/utils/flameConverter.d.ts +10 -3
- package/dist/audio/AnimationPlayer.d.ts +46 -0
- package/dist/audio/StreamingAudioPlayer.d.ts +93 -0
- package/dist/config/app-config.d.ts +5 -1
- package/dist/config/constants.d.ts +7 -1
- package/dist/config/sdk-config-loader.d.ts +11 -3
- package/dist/core/Avatar.d.ts +10 -0
- package/dist/core/AvatarController.d.ts +164 -2
- package/dist/core/AvatarDownloader.d.ts +10 -0
- package/dist/core/AvatarManager.d.ts +27 -1
- package/dist/core/AvatarSDK.d.ts +27 -0
- package/dist/core/AvatarView.d.ts +148 -3
- package/dist/core/NetworkLayer.d.ts +6 -0
- package/dist/generated/common/v1/models.d.ts +8 -1
- package/dist/generated/driveningress/v1/driveningress.d.ts +11 -1
- package/dist/generated/driveningress/v2/driveningress.d.ts +5 -2
- package/dist/generated/google/protobuf/struct.d.ts +38 -5
- package/dist/generated/google/protobuf/timestamp.d.ts +102 -1
- package/dist/{index-CF8Fvg7k.js → index-BT9yxWW8.js} +1468 -30
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1 -1
- package/dist/renderer/RenderSystem.d.ts +8 -0
- package/dist/renderer/covariance.d.ts +11 -0
- package/dist/renderer/sortSplats.d.ts +10 -0
- package/dist/renderer/webgl/reorderData.d.ts +12 -0
- package/dist/renderer/webgl/webglRenderer.d.ts +53 -0
- package/dist/renderer/webgpu/webgpuRenderer.d.ts +38 -0
- package/dist/types/character-settings.d.ts +4 -0
- package/dist/types/character.d.ts +9 -3
- package/dist/types/index.d.ts +56 -23
- package/dist/utils/animation-interpolation.d.ts +30 -5
- package/dist/utils/client-id.d.ts +5 -0
- package/dist/utils/conversationId.d.ts +18 -0
- package/dist/utils/error-utils.d.ts +24 -1
- package/dist/utils/id-manager.d.ts +26 -0
- package/dist/utils/logger.d.ts +4 -1
- package/dist/utils/posthog-tracker.d.ts +27 -5
- package/dist/utils/pwa-cache-manager.d.ts +36 -0
- package/dist/utils/usage-tracker.d.ts +17 -2
- package/dist/vite.d.ts +16 -1
- package/dist/wasm/avatarCoreAdapter.d.ts +145 -0
- package/dist/wasm/avatarCoreMemory.d.ts +52 -0
- package/package.json +3 -3
|
@@ -2,6 +2,12 @@ var __defProp = Object.defineProperty;
|
|
|
2
2
|
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
3
|
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
4
|
class Avatar {
|
|
5
|
+
/**
|
|
6
|
+
* 构造函数(内部使用)
|
|
7
|
+
* @param id 数字人 ID
|
|
8
|
+
* @param characterMeta 角色元数据
|
|
9
|
+
* @param resources 资源数据
|
|
10
|
+
*/
|
|
5
11
|
constructor(id, characterMeta, resources) {
|
|
6
12
|
__publicField(this, "id");
|
|
7
13
|
__publicField(this, "characterMeta");
|
|
@@ -10,15 +16,27 @@ class Avatar {
|
|
|
10
16
|
this.characterMeta = characterMeta;
|
|
11
17
|
this.resources = resources;
|
|
12
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Get character metadata
|
|
21
|
+
* @returns Character metadata, including all configuration information (version, resource URLs, camera config, etc.)
|
|
22
|
+
*/
|
|
13
23
|
getCharacterMeta() {
|
|
14
24
|
return this.characterMeta;
|
|
15
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* 更新角色元数据(内部使用)
|
|
28
|
+
* @internal
|
|
29
|
+
*/
|
|
16
30
|
updateCharacterMeta(newMeta) {
|
|
17
31
|
this.characterMeta = newMeta;
|
|
18
32
|
if (this.resources.characterSettings !== newMeta.characterSettings) {
|
|
19
33
|
this.resources.characterSettings = newMeta.characterSettings;
|
|
20
34
|
}
|
|
21
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* 获取资源数据(内部使用)
|
|
38
|
+
* @internal
|
|
39
|
+
*/
|
|
22
40
|
getResources() {
|
|
23
41
|
return this.resources;
|
|
24
42
|
}
|
|
@@ -346,6 +364,9 @@ class BinaryWriter {
|
|
|
346
364
|
this.chunks = [];
|
|
347
365
|
this.buf = [];
|
|
348
366
|
}
|
|
367
|
+
/**
|
|
368
|
+
* Return all bytes written and reset this writer.
|
|
369
|
+
*/
|
|
349
370
|
finish() {
|
|
350
371
|
if (this.buf.length) {
|
|
351
372
|
this.chunks.push(new Uint8Array(this.buf));
|
|
@@ -363,12 +384,22 @@ class BinaryWriter {
|
|
|
363
384
|
this.chunks = [];
|
|
364
385
|
return bytes;
|
|
365
386
|
}
|
|
387
|
+
/**
|
|
388
|
+
* Start a new fork for length-delimited data like a message
|
|
389
|
+
* or a packed repeated field.
|
|
390
|
+
*
|
|
391
|
+
* Must be joined later with `join()`.
|
|
392
|
+
*/
|
|
366
393
|
fork() {
|
|
367
394
|
this.stack.push({ chunks: this.chunks, buf: this.buf });
|
|
368
395
|
this.chunks = [];
|
|
369
396
|
this.buf = [];
|
|
370
397
|
return this;
|
|
371
398
|
}
|
|
399
|
+
/**
|
|
400
|
+
* Join the last fork. Write its length and bytes, then
|
|
401
|
+
* return to the previous state.
|
|
402
|
+
*/
|
|
372
403
|
join() {
|
|
373
404
|
let chunk = this.finish();
|
|
374
405
|
let prev = this.stack.pop();
|
|
@@ -379,9 +410,19 @@ class BinaryWriter {
|
|
|
379
410
|
this.uint32(chunk.byteLength);
|
|
380
411
|
return this.raw(chunk);
|
|
381
412
|
}
|
|
413
|
+
/**
|
|
414
|
+
* Writes a tag (field number and wire type).
|
|
415
|
+
*
|
|
416
|
+
* Equivalent to `uint32( (fieldNo << 3 | type) >>> 0 )`.
|
|
417
|
+
*
|
|
418
|
+
* Generated code should compute the tag ahead of time and call `uint32()`.
|
|
419
|
+
*/
|
|
382
420
|
tag(fieldNo, type) {
|
|
383
421
|
return this.uint32((fieldNo << 3 | type) >>> 0);
|
|
384
422
|
}
|
|
423
|
+
/**
|
|
424
|
+
* Write a chunk of raw bytes.
|
|
425
|
+
*/
|
|
385
426
|
raw(chunk) {
|
|
386
427
|
if (this.buf.length) {
|
|
387
428
|
this.chunks.push(new Uint8Array(this.buf));
|
|
@@ -390,6 +431,9 @@ class BinaryWriter {
|
|
|
390
431
|
this.chunks.push(chunk);
|
|
391
432
|
return this;
|
|
392
433
|
}
|
|
434
|
+
/**
|
|
435
|
+
* Write a `uint32` value, an unsigned 32 bit varint.
|
|
436
|
+
*/
|
|
393
437
|
uint32(value) {
|
|
394
438
|
assertUInt32(value);
|
|
395
439
|
while (value > 127) {
|
|
@@ -399,75 +443,117 @@ class BinaryWriter {
|
|
|
399
443
|
this.buf.push(value);
|
|
400
444
|
return this;
|
|
401
445
|
}
|
|
446
|
+
/**
|
|
447
|
+
* Write a `int32` value, a signed 32 bit varint.
|
|
448
|
+
*/
|
|
402
449
|
int32(value) {
|
|
403
450
|
assertInt32(value);
|
|
404
451
|
varint32write(value, this.buf);
|
|
405
452
|
return this;
|
|
406
453
|
}
|
|
454
|
+
/**
|
|
455
|
+
* Write a `bool` value, a variant.
|
|
456
|
+
*/
|
|
407
457
|
bool(value) {
|
|
408
458
|
this.buf.push(value ? 1 : 0);
|
|
409
459
|
return this;
|
|
410
460
|
}
|
|
461
|
+
/**
|
|
462
|
+
* Write a `bytes` value, length-delimited arbitrary data.
|
|
463
|
+
*/
|
|
411
464
|
bytes(value) {
|
|
412
465
|
this.uint32(value.byteLength);
|
|
413
466
|
return this.raw(value);
|
|
414
467
|
}
|
|
468
|
+
/**
|
|
469
|
+
* Write a `string` value, length-delimited data converted to UTF-8 text.
|
|
470
|
+
*/
|
|
415
471
|
string(value) {
|
|
416
472
|
let chunk = this.encodeUtf8(value);
|
|
417
473
|
this.uint32(chunk.byteLength);
|
|
418
474
|
return this.raw(chunk);
|
|
419
475
|
}
|
|
476
|
+
/**
|
|
477
|
+
* Write a `float` value, 32-bit floating point number.
|
|
478
|
+
*/
|
|
420
479
|
float(value) {
|
|
421
480
|
assertFloat32(value);
|
|
422
481
|
let chunk = new Uint8Array(4);
|
|
423
482
|
new DataView(chunk.buffer).setFloat32(0, value, true);
|
|
424
483
|
return this.raw(chunk);
|
|
425
484
|
}
|
|
485
|
+
/**
|
|
486
|
+
* Write a `double` value, a 64-bit floating point number.
|
|
487
|
+
*/
|
|
426
488
|
double(value) {
|
|
427
489
|
let chunk = new Uint8Array(8);
|
|
428
490
|
new DataView(chunk.buffer).setFloat64(0, value, true);
|
|
429
491
|
return this.raw(chunk);
|
|
430
492
|
}
|
|
493
|
+
/**
|
|
494
|
+
* Write a `fixed32` value, an unsigned, fixed-length 32-bit integer.
|
|
495
|
+
*/
|
|
431
496
|
fixed32(value) {
|
|
432
497
|
assertUInt32(value);
|
|
433
498
|
let chunk = new Uint8Array(4);
|
|
434
499
|
new DataView(chunk.buffer).setUint32(0, value, true);
|
|
435
500
|
return this.raw(chunk);
|
|
436
501
|
}
|
|
502
|
+
/**
|
|
503
|
+
* Write a `sfixed32` value, a signed, fixed-length 32-bit integer.
|
|
504
|
+
*/
|
|
437
505
|
sfixed32(value) {
|
|
438
506
|
assertInt32(value);
|
|
439
507
|
let chunk = new Uint8Array(4);
|
|
440
508
|
new DataView(chunk.buffer).setInt32(0, value, true);
|
|
441
509
|
return this.raw(chunk);
|
|
442
510
|
}
|
|
511
|
+
/**
|
|
512
|
+
* Write a `sint32` value, a signed, zigzag-encoded 32-bit varint.
|
|
513
|
+
*/
|
|
443
514
|
sint32(value) {
|
|
444
515
|
assertInt32(value);
|
|
445
516
|
value = (value << 1 ^ value >> 31) >>> 0;
|
|
446
517
|
varint32write(value, this.buf);
|
|
447
518
|
return this;
|
|
448
519
|
}
|
|
520
|
+
/**
|
|
521
|
+
* Write a `fixed64` value, a signed, fixed-length 64-bit integer.
|
|
522
|
+
*/
|
|
449
523
|
sfixed64(value) {
|
|
450
524
|
let chunk = new Uint8Array(8), view = new DataView(chunk.buffer), tc = protoInt64.enc(value);
|
|
451
525
|
view.setInt32(0, tc.lo, true);
|
|
452
526
|
view.setInt32(4, tc.hi, true);
|
|
453
527
|
return this.raw(chunk);
|
|
454
528
|
}
|
|
529
|
+
/**
|
|
530
|
+
* Write a `fixed64` value, an unsigned, fixed-length 64 bit integer.
|
|
531
|
+
*/
|
|
455
532
|
fixed64(value) {
|
|
456
533
|
let chunk = new Uint8Array(8), view = new DataView(chunk.buffer), tc = protoInt64.uEnc(value);
|
|
457
534
|
view.setInt32(0, tc.lo, true);
|
|
458
535
|
view.setInt32(4, tc.hi, true);
|
|
459
536
|
return this.raw(chunk);
|
|
460
537
|
}
|
|
538
|
+
/**
|
|
539
|
+
* Write a `int64` value, a signed 64-bit varint.
|
|
540
|
+
*/
|
|
461
541
|
int64(value) {
|
|
462
542
|
let tc = protoInt64.enc(value);
|
|
463
543
|
varint64write(tc.lo, tc.hi, this.buf);
|
|
464
544
|
return this;
|
|
465
545
|
}
|
|
546
|
+
/**
|
|
547
|
+
* Write a `sint64` value, a signed, zig-zag-encoded 64-bit varint.
|
|
548
|
+
*/
|
|
466
549
|
sint64(value) {
|
|
467
550
|
const tc = protoInt64.enc(value), sign = tc.hi >> 31, lo2 = tc.lo << 1 ^ sign, hi2 = (tc.hi << 1 | tc.lo >>> 31) ^ sign;
|
|
468
551
|
varint64write(lo2, hi2, this.buf);
|
|
469
552
|
return this;
|
|
470
553
|
}
|
|
554
|
+
/**
|
|
555
|
+
* Write a `uint64` value, an unsigned 64-bit varint.
|
|
556
|
+
*/
|
|
471
557
|
uint64(value) {
|
|
472
558
|
const tc = protoInt64.uEnc(value);
|
|
473
559
|
varint64write(tc.lo, tc.hi, this.buf);
|
|
@@ -484,12 +570,21 @@ class BinaryReader {
|
|
|
484
570
|
this.pos = 0;
|
|
485
571
|
this.view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
486
572
|
}
|
|
573
|
+
/**
|
|
574
|
+
* Reads a tag - field number and wire type.
|
|
575
|
+
*/
|
|
487
576
|
tag() {
|
|
488
577
|
let tag = this.uint32(), fieldNo = tag >>> 3, wireType = tag & 7;
|
|
489
578
|
if (fieldNo <= 0 || wireType < 0 || wireType > 5)
|
|
490
579
|
throw new Error("illegal tag: field no " + fieldNo + " wire type " + wireType);
|
|
491
580
|
return [fieldNo, wireType];
|
|
492
581
|
}
|
|
582
|
+
/**
|
|
583
|
+
* Skip one element and return the skipped data.
|
|
584
|
+
*
|
|
585
|
+
* When skipping StartGroup, provide the tags field number to check for
|
|
586
|
+
* matching field number in the EndGroup tag.
|
|
587
|
+
*/
|
|
493
588
|
skip(wireType, fieldNo) {
|
|
494
589
|
let start = this.pos;
|
|
495
590
|
switch (wireType) {
|
|
@@ -524,23 +619,41 @@ class BinaryReader {
|
|
|
524
619
|
this.assertBounds();
|
|
525
620
|
return this.buf.subarray(start, this.pos);
|
|
526
621
|
}
|
|
622
|
+
/**
|
|
623
|
+
* Throws error if position in byte array is out of range.
|
|
624
|
+
*/
|
|
527
625
|
assertBounds() {
|
|
528
626
|
if (this.pos > this.len)
|
|
529
627
|
throw new RangeError("premature EOF");
|
|
530
628
|
}
|
|
629
|
+
/**
|
|
630
|
+
* Read a `int32` field, a signed 32 bit varint.
|
|
631
|
+
*/
|
|
531
632
|
int32() {
|
|
532
633
|
return this.uint32() | 0;
|
|
533
634
|
}
|
|
635
|
+
/**
|
|
636
|
+
* Read a `sint32` field, a signed, zigzag-encoded 32-bit varint.
|
|
637
|
+
*/
|
|
534
638
|
sint32() {
|
|
535
639
|
let zze = this.uint32();
|
|
536
640
|
return zze >>> 1 ^ -(zze & 1);
|
|
537
641
|
}
|
|
642
|
+
/**
|
|
643
|
+
* Read a `int64` field, a signed 64-bit varint.
|
|
644
|
+
*/
|
|
538
645
|
int64() {
|
|
539
646
|
return protoInt64.dec(...this.varint64());
|
|
540
647
|
}
|
|
648
|
+
/**
|
|
649
|
+
* Read a `uint64` field, an unsigned 64-bit varint.
|
|
650
|
+
*/
|
|
541
651
|
uint64() {
|
|
542
652
|
return protoInt64.uDec(...this.varint64());
|
|
543
653
|
}
|
|
654
|
+
/**
|
|
655
|
+
* Read a `sint64` field, a signed, zig-zag-encoded 64-bit varint.
|
|
656
|
+
*/
|
|
544
657
|
sint64() {
|
|
545
658
|
let [lo2, hi2] = this.varint64();
|
|
546
659
|
let s2 = -(lo2 & 1);
|
|
@@ -548,34 +661,61 @@ class BinaryReader {
|
|
|
548
661
|
hi2 = hi2 >>> 1 ^ s2;
|
|
549
662
|
return protoInt64.dec(lo2, hi2);
|
|
550
663
|
}
|
|
664
|
+
/**
|
|
665
|
+
* Read a `bool` field, a variant.
|
|
666
|
+
*/
|
|
551
667
|
bool() {
|
|
552
668
|
let [lo2, hi2] = this.varint64();
|
|
553
669
|
return lo2 !== 0 || hi2 !== 0;
|
|
554
670
|
}
|
|
671
|
+
/**
|
|
672
|
+
* Read a `fixed32` field, an unsigned, fixed-length 32-bit integer.
|
|
673
|
+
*/
|
|
555
674
|
fixed32() {
|
|
556
675
|
return this.view.getUint32((this.pos += 4) - 4, true);
|
|
557
676
|
}
|
|
677
|
+
/**
|
|
678
|
+
* Read a `sfixed32` field, a signed, fixed-length 32-bit integer.
|
|
679
|
+
*/
|
|
558
680
|
sfixed32() {
|
|
559
681
|
return this.view.getInt32((this.pos += 4) - 4, true);
|
|
560
682
|
}
|
|
683
|
+
/**
|
|
684
|
+
* Read a `fixed64` field, an unsigned, fixed-length 64 bit integer.
|
|
685
|
+
*/
|
|
561
686
|
fixed64() {
|
|
562
687
|
return protoInt64.uDec(this.sfixed32(), this.sfixed32());
|
|
563
688
|
}
|
|
689
|
+
/**
|
|
690
|
+
* Read a `fixed64` field, a signed, fixed-length 64-bit integer.
|
|
691
|
+
*/
|
|
564
692
|
sfixed64() {
|
|
565
693
|
return protoInt64.dec(this.sfixed32(), this.sfixed32());
|
|
566
694
|
}
|
|
695
|
+
/**
|
|
696
|
+
* Read a `float` field, 32-bit floating point number.
|
|
697
|
+
*/
|
|
567
698
|
float() {
|
|
568
699
|
return this.view.getFloat32((this.pos += 4) - 4, true);
|
|
569
700
|
}
|
|
701
|
+
/**
|
|
702
|
+
* Read a `double` field, a 64-bit floating point number.
|
|
703
|
+
*/
|
|
570
704
|
double() {
|
|
571
705
|
return this.view.getFloat64((this.pos += 8) - 8, true);
|
|
572
706
|
}
|
|
707
|
+
/**
|
|
708
|
+
* Read a `bytes` field, length-delimited arbitrary data.
|
|
709
|
+
*/
|
|
573
710
|
bytes() {
|
|
574
711
|
let len = this.uint32(), start = this.pos;
|
|
575
712
|
this.pos += len;
|
|
576
713
|
this.assertBounds();
|
|
577
714
|
return this.buf.subarray(start, start + len);
|
|
578
715
|
}
|
|
716
|
+
/**
|
|
717
|
+
* Read a `string` field, length-delimited data converted to UTF-8 text.
|
|
718
|
+
*/
|
|
579
719
|
string() {
|
|
580
720
|
return this.decodeUtf8(this.bytes());
|
|
581
721
|
}
|
|
@@ -1266,6 +1406,7 @@ function getPostHogConfig(_environment) {
|
|
|
1266
1406
|
host: POSTHOG_HOST_INTL,
|
|
1267
1407
|
apiKey: POSTHOG_API_KEY_INTL,
|
|
1268
1408
|
disableCompression: false
|
|
1409
|
+
// 国外官方 PostHog 支持压缩,保持启用
|
|
1269
1410
|
};
|
|
1270
1411
|
}
|
|
1271
1412
|
function hasDebugParam() {
|
|
@@ -1279,6 +1420,7 @@ function isDebugMode() {
|
|
|
1279
1420
|
return hasDebugParam();
|
|
1280
1421
|
}
|
|
1281
1422
|
const APP_CONFIG = {
|
|
1423
|
+
// Dynamic debug mode check (includes URL parameter)
|
|
1282
1424
|
get debug() {
|
|
1283
1425
|
return isDebugMode();
|
|
1284
1426
|
},
|
|
@@ -1291,10 +1433,14 @@ const APP_CONFIG = {
|
|
|
1291
1433
|
},
|
|
1292
1434
|
animation: {
|
|
1293
1435
|
fps: 25
|
|
1436
|
+
// 动画帧率(从 CharacterMeta 加载动画资源)
|
|
1294
1437
|
},
|
|
1295
1438
|
audio: {
|
|
1296
1439
|
sampleRate: 16e3
|
|
1440
|
+
// 音频采样率(后端要求 16kHz)
|
|
1297
1441
|
},
|
|
1442
|
+
// FLAME 全局模板资源 CDN
|
|
1443
|
+
// 这些资源是所有角色共享的基础模板
|
|
1298
1444
|
flame: {
|
|
1299
1445
|
cdnBaseUrl: "https://cdn.spatialwalk.top/public",
|
|
1300
1446
|
resources: {
|
|
@@ -1316,6 +1462,7 @@ function convertProtoFlameToWasmParams(protoFlame) {
|
|
|
1316
1462
|
eyelid: protoFlame.eyeLid || [0, 0],
|
|
1317
1463
|
expr_params: protoFlame.expression || [],
|
|
1318
1464
|
shape_params: [],
|
|
1465
|
+
// Realtime doesn't provide shape params, use default
|
|
1319
1466
|
has_eyelid: (((_a = protoFlame.eyeLid) == null ? void 0 : _a.length) || 0) > 0
|
|
1320
1467
|
};
|
|
1321
1468
|
}
|
|
@@ -5965,6 +6112,12 @@ function requireStackframe() {
|
|
|
5965
6112
|
var CHROME_IE_STACK_REGEXP = /^\s*at .*(\S+:\d+|\(native\))/m;
|
|
5966
6113
|
var SAFARI_NATIVE_CODE_REGEXP = /^(eval@)?(\[native code])?$/;
|
|
5967
6114
|
return {
|
|
6115
|
+
/**
|
|
6116
|
+
* Given an Error object, extract the most information from it.
|
|
6117
|
+
*
|
|
6118
|
+
* @param {Error} error object
|
|
6119
|
+
* @return {Array} of StackFrames
|
|
6120
|
+
*/
|
|
5968
6121
|
parse: function ErrorStackParser$$parse(error) {
|
|
5969
6122
|
if (typeof error.stacktrace !== "undefined" || typeof error["opera#sourceloc"] !== "undefined") {
|
|
5970
6123
|
return this.parseOpera(error);
|
|
@@ -5976,6 +6129,7 @@ function requireStackframe() {
|
|
|
5976
6129
|
throw new Error("Cannot parse given Error object");
|
|
5977
6130
|
}
|
|
5978
6131
|
},
|
|
6132
|
+
// Separate line and column numbers from a string of the form: (URI:Line:Column)
|
|
5979
6133
|
extractLocation: function ErrorStackParser$$extractLocation(urlLike) {
|
|
5980
6134
|
if (urlLike.indexOf(":") === -1) {
|
|
5981
6135
|
return [urlLike];
|
|
@@ -6078,6 +6232,7 @@ function requireStackframe() {
|
|
|
6078
6232
|
}
|
|
6079
6233
|
return result2;
|
|
6080
6234
|
},
|
|
6235
|
+
// Opera 10.65+ Error.stack very similar to FF/Safari
|
|
6081
6236
|
parseOpera11: function ErrorStackParser$$parseOpera11(error) {
|
|
6082
6237
|
var filtered = error.stack.split("\n").filter(function(line) {
|
|
6083
6238
|
return !!line.match(FIREFOX_SAFARI_STACK_REGEXP) && !line.match(/^Error created at/);
|
|
@@ -6964,6 +7119,7 @@ function createLogg(context) {
|
|
|
6964
7119
|
column: stacks[0].column
|
|
6965
7120
|
});
|
|
6966
7121
|
},
|
|
7122
|
+
// Placeholder implementations, will be overridden below
|
|
6967
7123
|
debug: () => {
|
|
6968
7124
|
},
|
|
6969
7125
|
verbose: () => {
|
|
@@ -6976,6 +7132,7 @@ function createLogg(context) {
|
|
|
6976
7132
|
},
|
|
6977
7133
|
warn: () => {
|
|
6978
7134
|
},
|
|
7135
|
+
// TODO: remove in next major release
|
|
6979
7136
|
withTimeFormat: (_2) => {
|
|
6980
7137
|
const logger2 = logObj.child();
|
|
6981
7138
|
return logger2;
|
|
@@ -7115,10 +7272,22 @@ var ResourceType = /* @__PURE__ */ ((ResourceType2) => {
|
|
|
7115
7272
|
function extractResourceUrls(meta) {
|
|
7116
7273
|
var _a, _b, _c, _d, _e2, _f, _g, _h, _i2, _j, _k;
|
|
7117
7274
|
return {
|
|
7118
|
-
[
|
|
7119
|
-
|
|
7120
|
-
|
|
7121
|
-
|
|
7275
|
+
[
|
|
7276
|
+
"camera"
|
|
7277
|
+
/* CAMERA */
|
|
7278
|
+
]: ((_b = (_a = meta.camera) == null ? void 0 : _a.resource) == null ? void 0 : _b.remote) || null,
|
|
7279
|
+
[
|
|
7280
|
+
"frameIdle"
|
|
7281
|
+
/* ANIMATION_IDLE */
|
|
7282
|
+
]: ((_e2 = (_d = (_c = meta.animations) == null ? void 0 : _c.frameIdle) == null ? void 0 : _d.resource) == null ? void 0 : _e2.remote) || null,
|
|
7283
|
+
[
|
|
7284
|
+
"shape"
|
|
7285
|
+
/* MODEL_SHAPE */
|
|
7286
|
+
]: ((_h = (_g = (_f = meta.models) == null ? void 0 : _f.shape) == null ? void 0 : _g.resource) == null ? void 0 : _h.remote) || null,
|
|
7287
|
+
[
|
|
7288
|
+
"gsStandard"
|
|
7289
|
+
/* MODEL_GS */
|
|
7290
|
+
]: ((_k = (_j = (_i2 = meta.models) == null ? void 0 : _i2.gsStandard) == null ? void 0 : _j.resource) == null ? void 0 : _k.remote) || null
|
|
7122
7291
|
};
|
|
7123
7292
|
}
|
|
7124
7293
|
var Environment = /* @__PURE__ */ ((Environment2) => {
|
|
@@ -7313,6 +7482,7 @@ class IdManager {
|
|
|
7313
7482
|
});
|
|
7314
7483
|
this.ids.clientId = getOrCreateClientId();
|
|
7315
7484
|
}
|
|
7485
|
+
// ========== 全局 ID ==========
|
|
7316
7486
|
getClientId() {
|
|
7317
7487
|
return this.ids.clientId;
|
|
7318
7488
|
}
|
|
@@ -7334,10 +7504,17 @@ class IdManager {
|
|
|
7334
7504
|
getSessionToken() {
|
|
7335
7505
|
return this.ids.sessionToken;
|
|
7336
7506
|
}
|
|
7507
|
+
// ========== 连接级别 ID ==========
|
|
7508
|
+
/**
|
|
7509
|
+
* 生成新的 connectionId(用于 WebSocket 连接)
|
|
7510
|
+
*/
|
|
7337
7511
|
generateConnectionId() {
|
|
7338
7512
|
this.ids.connectionId = generateConversationId();
|
|
7339
7513
|
return this.ids.connectionId;
|
|
7340
7514
|
}
|
|
7515
|
+
/**
|
|
7516
|
+
* 设置 connectionId(用于从服务器接收到的 connectionId)
|
|
7517
|
+
*/
|
|
7341
7518
|
setConnectionId(connectionId) {
|
|
7342
7519
|
this.ids.connectionId = connectionId;
|
|
7343
7520
|
}
|
|
@@ -7347,6 +7524,10 @@ class IdManager {
|
|
|
7347
7524
|
clearConnectionId() {
|
|
7348
7525
|
this.ids.connectionId = null;
|
|
7349
7526
|
}
|
|
7527
|
+
// ========== 会话级别 ID ==========
|
|
7528
|
+
/**
|
|
7529
|
+
* 生成新的 conversationId(用于每次对话)
|
|
7530
|
+
*/
|
|
7350
7531
|
generateNewConversationId() {
|
|
7351
7532
|
this.ids.conversationId = generateConversationId();
|
|
7352
7533
|
return this.ids.conversationId;
|
|
@@ -7360,9 +7541,16 @@ class IdManager {
|
|
|
7360
7541
|
clearConversationId() {
|
|
7361
7542
|
this.ids.conversationId = null;
|
|
7362
7543
|
}
|
|
7544
|
+
// ========== 批量获取(用于 PostHog 日志)==========
|
|
7545
|
+
/**
|
|
7546
|
+
* 获取所有 ID(用于日志上报)
|
|
7547
|
+
*/
|
|
7363
7548
|
getAllIds() {
|
|
7364
7549
|
return { ...this.ids };
|
|
7365
7550
|
}
|
|
7551
|
+
/**
|
|
7552
|
+
* 获取公共日志参数(包含所有必要的 ID)
|
|
7553
|
+
*/
|
|
7366
7554
|
getLogContext() {
|
|
7367
7555
|
return {
|
|
7368
7556
|
client_id: this.ids.clientId,
|
|
@@ -7372,6 +7560,9 @@ class IdManager {
|
|
|
7372
7560
|
...this.ids.conversationId && { conversation_id: this.ids.conversationId }
|
|
7373
7561
|
};
|
|
7374
7562
|
}
|
|
7563
|
+
/**
|
|
7564
|
+
* 清理所有 ID(用于测试或重置)
|
|
7565
|
+
*/
|
|
7375
7566
|
clear() {
|
|
7376
7567
|
this.ids.userId = null;
|
|
7377
7568
|
this.ids.appId = null;
|
|
@@ -7413,10 +7604,15 @@ function initializePostHog(environment, version) {
|
|
|
7413
7604
|
api_host: host,
|
|
7414
7605
|
person_profiles: "identified_only",
|
|
7415
7606
|
capture_pageview: false,
|
|
7607
|
+
// SDK 不需要自动捕获页面浏览
|
|
7416
7608
|
capture_pageleave: false,
|
|
7609
|
+
// SDK 不需要自动捕获页面离开
|
|
7417
7610
|
disable_compression: disableCompression,
|
|
7611
|
+
// 根据环境配置压缩:国内禁用,国外启用
|
|
7418
7612
|
disable_session_recording: true,
|
|
7613
|
+
// 禁用 Session Recording,避免额外的请求和错误
|
|
7419
7614
|
autocapture: false,
|
|
7615
|
+
// 禁用自动捕获,只上报 SDK 自定义事件
|
|
7420
7616
|
loaded: (posthogInstance) => {
|
|
7421
7617
|
logger.log(`[PostHog] Initialized successfully - environment: ${environment}, host: ${host}, instance: ${SDK_POSTHOG_INSTANCE_NAME}`);
|
|
7422
7618
|
isInitialized = true;
|
|
@@ -7502,15 +7698,19 @@ function trackEvent(event, level = "info", contents = {}) {
|
|
|
7502
7698
|
try {
|
|
7503
7699
|
const logContext = idManager.getLogContext();
|
|
7504
7700
|
const properties = {
|
|
7701
|
+
// 基础属性
|
|
7505
7702
|
service_module: "sdk",
|
|
7506
7703
|
platform: "Web",
|
|
7507
7704
|
sdk_version: sdkVersion,
|
|
7508
7705
|
level,
|
|
7509
7706
|
environment: currentEnvironment || "unknown",
|
|
7707
|
+
// ID 信息
|
|
7510
7708
|
app_id: logContext.app_id,
|
|
7511
7709
|
user_id: logContext.user_id,
|
|
7512
7710
|
client_id: logContext.client_id,
|
|
7711
|
+
// 时间戳
|
|
7513
7712
|
timestamp: Date.now(),
|
|
7713
|
+
// 自定义内容
|
|
7514
7714
|
...contents
|
|
7515
7715
|
};
|
|
7516
7716
|
if (logContext.connection_id) {
|
|
@@ -7572,6 +7772,10 @@ const _AnimationPlayer = class _AnimationPlayer {
|
|
|
7572
7772
|
__publicField(this, "onEndedCallback");
|
|
7573
7773
|
__publicField(this, "useStreaming", false);
|
|
7574
7774
|
}
|
|
7775
|
+
/**
|
|
7776
|
+
* 解锁音频上下文(Safari 自动播放策略)
|
|
7777
|
+
* 必须在用户交互事件(如 click)中调用
|
|
7778
|
+
*/
|
|
7575
7779
|
static async unlockAudioContext() {
|
|
7576
7780
|
if (_AnimationPlayer.audioUnlocked) {
|
|
7577
7781
|
return;
|
|
@@ -7589,6 +7793,9 @@ const _AnimationPlayer = class _AnimationPlayer {
|
|
|
7589
7793
|
logger.warn("⚠️ Failed to unlock audio context:", err);
|
|
7590
7794
|
}
|
|
7591
7795
|
}
|
|
7796
|
+
/**
|
|
7797
|
+
* Initialize with HTMLAudioElement (traditional way)
|
|
7798
|
+
*/
|
|
7592
7799
|
async initialize(audioUrl, onEnded) {
|
|
7593
7800
|
this.onEndedCallback = onEnded;
|
|
7594
7801
|
this.useStreaming = false;
|
|
@@ -7604,6 +7811,10 @@ const _AnimationPlayer = class _AnimationPlayer {
|
|
|
7604
7811
|
this.audio.load();
|
|
7605
7812
|
});
|
|
7606
7813
|
}
|
|
7814
|
+
/**
|
|
7815
|
+
* Initialize with StreamingAudioPlayer (streaming way)
|
|
7816
|
+
* @deprecated 使用 prepareStreamingPlayer() 代替
|
|
7817
|
+
*/
|
|
7607
7818
|
async initializeStreaming(streamingPlayer, onEnded) {
|
|
7608
7819
|
this.streamingPlayer = streamingPlayer;
|
|
7609
7820
|
this.onEndedCallback = onEnded;
|
|
@@ -7614,17 +7825,27 @@ const _AnimationPlayer = class _AnimationPlayer {
|
|
|
7614
7825
|
(_a = this.onEndedCallback) == null ? void 0 : _a.call(this);
|
|
7615
7826
|
});
|
|
7616
7827
|
}
|
|
7828
|
+
/**
|
|
7829
|
+
* 检查流式播放器是否已准备好
|
|
7830
|
+
*/
|
|
7617
7831
|
isStreamingReady() {
|
|
7618
7832
|
return this.useStreaming && this.streamingPlayer !== null;
|
|
7619
7833
|
}
|
|
7834
|
+
/**
|
|
7835
|
+
* 获取流式播放器实例
|
|
7836
|
+
*/
|
|
7620
7837
|
getStreamingPlayer() {
|
|
7621
7838
|
return this.streamingPlayer;
|
|
7622
7839
|
}
|
|
7840
|
+
/**
|
|
7841
|
+
* 创建并初始化流式播放器
|
|
7842
|
+
* 在服务连接建立时调用
|
|
7843
|
+
*/
|
|
7623
7844
|
async createAndInitializeStreamingPlayer() {
|
|
7624
7845
|
if (this.streamingPlayer) {
|
|
7625
7846
|
return;
|
|
7626
7847
|
}
|
|
7627
|
-
const { StreamingAudioPlayer } = await import("./StreamingAudioPlayer-
|
|
7848
|
+
const { StreamingAudioPlayer } = await import("./StreamingAudioPlayer-DiIRp5nx.js");
|
|
7628
7849
|
const { AvatarSDK: AvatarSDK2 } = await Promise.resolve().then(() => AvatarSDK$1);
|
|
7629
7850
|
const audioFormat = AvatarSDK2.getAudioFormat();
|
|
7630
7851
|
this.streamingPlayer = new StreamingAudioPlayer({
|
|
@@ -7645,6 +7866,10 @@ const _AnimationPlayer = class _AnimationPlayer {
|
|
|
7645
7866
|
}
|
|
7646
7867
|
this.useStreaming = true;
|
|
7647
7868
|
}
|
|
7869
|
+
/**
|
|
7870
|
+
* 准备流式播放器(如果未创建则创建并初始化)
|
|
7871
|
+
* 停止之前的播放并更新结束回调
|
|
7872
|
+
*/
|
|
7648
7873
|
async prepareStreamingPlayer(onEnded) {
|
|
7649
7874
|
if (!this.streamingPlayer) {
|
|
7650
7875
|
await this.createAndInitializeStreamingPlayer();
|
|
@@ -7705,6 +7930,9 @@ const _AnimationPlayer = class _AnimationPlayer {
|
|
|
7705
7930
|
const currentTime = this.getCurrentTime();
|
|
7706
7931
|
return Math.floor(currentTime * this.fps);
|
|
7707
7932
|
}
|
|
7933
|
+
/**
|
|
7934
|
+
* Get current playback time
|
|
7935
|
+
*/
|
|
7708
7936
|
getCurrentTime() {
|
|
7709
7937
|
var _a, _b;
|
|
7710
7938
|
if (this.useStreaming) {
|
|
@@ -7712,6 +7940,9 @@ const _AnimationPlayer = class _AnimationPlayer {
|
|
|
7712
7940
|
}
|
|
7713
7941
|
return ((_b = this.audio) == null ? void 0 : _b.currentTime) ?? 0;
|
|
7714
7942
|
}
|
|
7943
|
+
/**
|
|
7944
|
+
* 添加音频块(仅用于流式播放)
|
|
7945
|
+
*/
|
|
7715
7946
|
addAudioChunk(audio, isLast = false) {
|
|
7716
7947
|
if (this.useStreaming && this.streamingPlayer) {
|
|
7717
7948
|
this.streamingPlayer.addChunk(audio, isLast);
|
|
@@ -7719,6 +7950,9 @@ const _AnimationPlayer = class _AnimationPlayer {
|
|
|
7719
7950
|
logger.warn("[AnimationPlayer] Cannot add audio chunk - streaming player not ready");
|
|
7720
7951
|
}
|
|
7721
7952
|
}
|
|
7953
|
+
/**
|
|
7954
|
+
* 暂停播放
|
|
7955
|
+
*/
|
|
7722
7956
|
pause() {
|
|
7723
7957
|
var _a;
|
|
7724
7958
|
if (this.useStreaming) {
|
|
@@ -7729,6 +7963,9 @@ const _AnimationPlayer = class _AnimationPlayer {
|
|
|
7729
7963
|
}
|
|
7730
7964
|
}
|
|
7731
7965
|
}
|
|
7966
|
+
/**
|
|
7967
|
+
* 继续播放
|
|
7968
|
+
*/
|
|
7732
7969
|
async resume() {
|
|
7733
7970
|
var _a;
|
|
7734
7971
|
if (this.useStreaming) {
|
|
@@ -7739,6 +7976,11 @@ const _AnimationPlayer = class _AnimationPlayer {
|
|
|
7739
7976
|
}
|
|
7740
7977
|
}
|
|
7741
7978
|
}
|
|
7979
|
+
/**
|
|
7980
|
+
* 设置音量 (0.0 - 1.0)
|
|
7981
|
+
* 注意:这仅控制数字人音频播放器的音量,不影响系统音量
|
|
7982
|
+
* @param volume 音量值,范围 0.0 到 1.0(0.0 为静音,1.0 为最大音量)
|
|
7983
|
+
*/
|
|
7742
7984
|
setVolume(volume) {
|
|
7743
7985
|
if (this.useStreaming && this.streamingPlayer) {
|
|
7744
7986
|
this.streamingPlayer.setVolume(volume);
|
|
@@ -7748,6 +7990,10 @@ const _AnimationPlayer = class _AnimationPlayer {
|
|
|
7748
7990
|
}
|
|
7749
7991
|
}
|
|
7750
7992
|
}
|
|
7993
|
+
/**
|
|
7994
|
+
* 获取当前音量
|
|
7995
|
+
* @returns 当前音量值 (0.0 - 1.0)
|
|
7996
|
+
*/
|
|
7751
7997
|
getVolume() {
|
|
7752
7998
|
var _a;
|
|
7753
7999
|
if (this.useStreaming && this.streamingPlayer) {
|
|
@@ -7793,6 +8039,7 @@ async function fetchSdkConfig(version) {
|
|
|
7793
8039
|
logger.log(`[SdkConfigLoader] Fetching SDK config from: ${configUrl}`);
|
|
7794
8040
|
const response = await fetch(configUrl, {
|
|
7795
8041
|
method: "GET"
|
|
8042
|
+
// GET 请求不需要 Content-Type header,避免触发 CORS 预检
|
|
7796
8043
|
});
|
|
7797
8044
|
if (!response.ok) {
|
|
7798
8045
|
throw new Error(`HTTP ${response.status} ${response.statusText}`);
|
|
@@ -7852,6 +8099,9 @@ class AvatarCoreMemoryManager {
|
|
|
7852
8099
|
this.allocatedPointers = /* @__PURE__ */ new Set();
|
|
7853
8100
|
this.structPointers = /* @__PURE__ */ new Map();
|
|
7854
8101
|
}
|
|
8102
|
+
/**
|
|
8103
|
+
* 分配 WASM 内存并复制数据 - 纯 API 方式
|
|
8104
|
+
*/
|
|
7855
8105
|
allocateAndCopy(arrayBuffer) {
|
|
7856
8106
|
const size = arrayBuffer.byteLength;
|
|
7857
8107
|
const ptr = this.module._malloc(size);
|
|
@@ -7863,6 +8113,9 @@ class AvatarCoreMemoryManager {
|
|
|
7863
8113
|
this.allocatedPointers.add(ptr);
|
|
7864
8114
|
return { ptr, size };
|
|
7865
8115
|
}
|
|
8116
|
+
/**
|
|
8117
|
+
* 创建 AvatarTemplateConfig 结构体
|
|
8118
|
+
*/
|
|
7866
8119
|
createTemplateConfig(resources) {
|
|
7867
8120
|
const structSize = 10 * 4;
|
|
7868
8121
|
const configPtr = this.module._malloc(structSize);
|
|
@@ -7909,6 +8162,9 @@ class AvatarCoreMemoryManager {
|
|
|
7909
8162
|
this.structPointers.set("template_config", configPtr);
|
|
7910
8163
|
return configPtr;
|
|
7911
8164
|
}
|
|
8165
|
+
/**
|
|
8166
|
+
* 创建 AvatarCharacterData 结构体 - 新版 Emscripten 方式
|
|
8167
|
+
*/
|
|
7912
8168
|
createCharacterData(shapeBuffer, plyBuffer, characterId) {
|
|
7913
8169
|
const structSize = 4 * 4;
|
|
7914
8170
|
const dataPtr = this.module._malloc(structSize);
|
|
@@ -7930,6 +8186,10 @@ class AvatarCoreMemoryManager {
|
|
|
7930
8186
|
this.structPointers.set(key, dataPtr);
|
|
7931
8187
|
return { dataPtr, shapePtr: shape.ptr, plyPtr: ply.ptr };
|
|
7932
8188
|
}
|
|
8189
|
+
/**
|
|
8190
|
+
* 读取 AvatarFlameParams 结构体数据
|
|
8191
|
+
* Used to extract current animation frame parameters from WASM memory
|
|
8192
|
+
*/
|
|
7933
8193
|
readFlameParams(paramsPtr) {
|
|
7934
8194
|
let byteOffset = 0;
|
|
7935
8195
|
const shape_params = [];
|
|
@@ -7985,6 +8245,9 @@ class AvatarCoreMemoryManager {
|
|
|
7985
8245
|
has_eyelid
|
|
7986
8246
|
};
|
|
7987
8247
|
}
|
|
8248
|
+
/**
|
|
8249
|
+
* 创建 AvatarFlameParams 结构体 - 新版 Emscripten 方式
|
|
8250
|
+
*/
|
|
7988
8251
|
createFlameParams(params) {
|
|
7989
8252
|
var _a, _b, _c, _d, _e2, _f, _g, _h;
|
|
7990
8253
|
const structSize = (300 + 100 + 3 + 3 + 3 + 3 + 6 + 2 + 1) * 4;
|
|
@@ -8037,6 +8300,21 @@ class AvatarCoreMemoryManager {
|
|
|
8037
8300
|
byteOffset += 4;
|
|
8038
8301
|
return paramsPtr;
|
|
8039
8302
|
}
|
|
8303
|
+
/**
|
|
8304
|
+
* 读取 AvatarSplatPointFlatArray 结构体数据(预计算协方差)
|
|
8305
|
+
*
|
|
8306
|
+
* 结构体布局:
|
|
8307
|
+
* - AvatarSplatPointFlat* points (0-3, 32位指针)
|
|
8308
|
+
* - uint32_t point_count (4-7)
|
|
8309
|
+
* - float compute_time_ms (8-11)
|
|
8310
|
+
*
|
|
8311
|
+
* 每个点布局 (52 bytes):
|
|
8312
|
+
* - position[3] (12 bytes)
|
|
8313
|
+
* - color[4] (16 bytes, RGBA)
|
|
8314
|
+
* - covariance[6] (24 bytes, 预计算好的协方差矩阵上三角)
|
|
8315
|
+
*
|
|
8316
|
+
* ⚠️ 使用 getValue 逐个读取,避免动态内存的 HEAPF32 detachment 问题
|
|
8317
|
+
*/
|
|
8040
8318
|
readSplatPointFlatArray(arrayPtr) {
|
|
8041
8319
|
if (!arrayPtr) {
|
|
8042
8320
|
throw new Error("Invalid array pointer");
|
|
@@ -8053,6 +8331,9 @@ class AvatarCoreMemoryManager {
|
|
|
8053
8331
|
flatData.set(this.module.HEAPF32.subarray(floatOffset, floatOffset + totalFloats));
|
|
8054
8332
|
return flatData;
|
|
8055
8333
|
}
|
|
8334
|
+
/**
|
|
8335
|
+
* 读取AvatarMeshData结构体数据
|
|
8336
|
+
*/
|
|
8056
8337
|
readMeshData(outputPtr) {
|
|
8057
8338
|
const verticesPtr = this.module.getValue(outputPtr + 0, "*");
|
|
8058
8339
|
const vertexCount = this.module.getValue(outputPtr + 4, "i32");
|
|
@@ -8090,12 +8371,18 @@ class AvatarCoreMemoryManager {
|
|
|
8090
8371
|
computeTime
|
|
8091
8372
|
};
|
|
8092
8373
|
}
|
|
8374
|
+
/**
|
|
8375
|
+
* 释放指定指针的内存
|
|
8376
|
+
*/
|
|
8093
8377
|
free(ptr) {
|
|
8094
8378
|
if (ptr && this.allocatedPointers.has(ptr)) {
|
|
8095
8379
|
this.module._free(ptr);
|
|
8096
8380
|
this.allocatedPointers.delete(ptr);
|
|
8097
8381
|
}
|
|
8098
8382
|
}
|
|
8383
|
+
/**
|
|
8384
|
+
* 释放结构体内存
|
|
8385
|
+
*/
|
|
8099
8386
|
freeStruct(name) {
|
|
8100
8387
|
const ptr = this.structPointers.get(name);
|
|
8101
8388
|
if (ptr) {
|
|
@@ -8103,6 +8390,9 @@ class AvatarCoreMemoryManager {
|
|
|
8103
8390
|
this.structPointers.delete(name);
|
|
8104
8391
|
}
|
|
8105
8392
|
}
|
|
8393
|
+
/**
|
|
8394
|
+
* 清理所有分配的内存
|
|
8395
|
+
*/
|
|
8106
8396
|
cleanup() {
|
|
8107
8397
|
for (const ptr of this.allocatedPointers) {
|
|
8108
8398
|
this.module._free(ptr);
|
|
@@ -8113,6 +8403,9 @@ class AvatarCoreMemoryManager {
|
|
|
8113
8403
|
}
|
|
8114
8404
|
this.structPointers.clear();
|
|
8115
8405
|
}
|
|
8406
|
+
/**
|
|
8407
|
+
* 获取内存使用统计
|
|
8408
|
+
*/
|
|
8116
8409
|
getMemoryStats() {
|
|
8117
8410
|
return {
|
|
8118
8411
|
allocatedPointers: this.allocatedPointers.size,
|
|
@@ -8123,22 +8416,36 @@ class AvatarCoreMemoryManager {
|
|
|
8123
8416
|
}
|
|
8124
8417
|
class AvatarCoreAdapter {
|
|
8125
8418
|
constructor(options = {}) {
|
|
8419
|
+
// 配置
|
|
8126
8420
|
__publicField(this, "options");
|
|
8127
8421
|
__publicField(this, "wasmConfig");
|
|
8422
|
+
// 核心组件
|
|
8423
|
+
// eslint-disable-next-line ts/no-explicit-any
|
|
8128
8424
|
__publicField(this, "wasmModule");
|
|
8129
8425
|
__publicField(this, "memoryManager");
|
|
8426
|
+
// Avatar Core handles
|
|
8130
8427
|
__publicField(this, "coreHandle");
|
|
8428
|
+
// Support multiple character handles (key: characterId, value: handle)
|
|
8131
8429
|
__publicField(this, "characterHandles", /* @__PURE__ */ new Map());
|
|
8430
|
+
// Animation handles per character (key: characterId, value: { handle, totalFrames })
|
|
8132
8431
|
__publicField(this, "animationHandles", /* @__PURE__ */ new Map());
|
|
8432
|
+
// Legacy single character support (deprecated, kept for backward compatibility)
|
|
8133
8433
|
__publicField(this, "characterHandle");
|
|
8134
8434
|
__publicField(this, "animationHandle");
|
|
8135
8435
|
__publicField(this, "totalFrames");
|
|
8436
|
+
// 状态管理
|
|
8136
8437
|
__publicField(this, "isInitialized");
|
|
8137
8438
|
__publicField(this, "isCharacterLoaded");
|
|
8439
|
+
// C API 函数包装
|
|
8440
|
+
// eslint-disable-next-line ts/no-explicit-any
|
|
8138
8441
|
__publicField(this, "api");
|
|
8442
|
+
// 性能监控
|
|
8139
8443
|
__publicField(this, "performanceMetrics");
|
|
8140
8444
|
__publicField(this, "wasmTime", 0);
|
|
8445
|
+
// 最近一次 WASM 计算耗时
|
|
8446
|
+
// 错误码映射
|
|
8141
8447
|
__publicField(this, "errorCodes");
|
|
8448
|
+
// 模型信息
|
|
8142
8449
|
__publicField(this, "flameInfo");
|
|
8143
8450
|
__publicField(this, "characterInfo");
|
|
8144
8451
|
this.options = {
|
|
@@ -8178,6 +8485,19 @@ class AvatarCoreAdapter {
|
|
|
8178
8485
|
6: "AVATAR_ERROR_COMPUTATION_FAILED"
|
|
8179
8486
|
};
|
|
8180
8487
|
}
|
|
8488
|
+
/**
|
|
8489
|
+
* 加载 WASM 模块并设置 API
|
|
8490
|
+
* 每次都创建全新的 WASM 实例,确保 C++ 内存是干净的
|
|
8491
|
+
*
|
|
8492
|
+
* 注意:这里使用动态 import() 导入 WASM 模块。
|
|
8493
|
+
* Vite 在构建时会自动为 WASM 文件和 JS glue 代码添加 hash(如 avatar_core_wasm-CxWuw7eS.wasm),
|
|
8494
|
+
* 确保浏览器缓存与版本一致,不会有缓存问题。
|
|
8495
|
+
*
|
|
8496
|
+
* Hash 的作用机制:
|
|
8497
|
+
* - WASM 文件内容变化 → hash 自动变化 → URL 变化 → 浏览器拉取新版本
|
|
8498
|
+
* - WASM 文件内容不变 → hash 保持不变 → URL 不变 → 浏览器使用缓存
|
|
8499
|
+
* - Emscripten 生成的 JS 内部会使用 hard-coded 的 hash 路径加载 .wasm 文件
|
|
8500
|
+
*/
|
|
8181
8501
|
async loadWASMModule() {
|
|
8182
8502
|
try {
|
|
8183
8503
|
const { default: AvatarCoreModule } = await import("./avatar_core_wasm-Dv943JJl.js");
|
|
@@ -8195,6 +8515,9 @@ class AvatarCoreAdapter {
|
|
|
8195
8515
|
throw new Error(`Failed to load WASM module: ${errorMessage}`);
|
|
8196
8516
|
}
|
|
8197
8517
|
}
|
|
8518
|
+
/**
|
|
8519
|
+
* 验证 WASM 模块功能
|
|
8520
|
+
*/
|
|
8198
8521
|
validateWASMModule() {
|
|
8199
8522
|
const requiredFunctions = ["cwrap", "_malloc", "_free", "setValue", "getValue", "writeArrayToMemory"];
|
|
8200
8523
|
for (const funcName of requiredFunctions) {
|
|
@@ -8204,6 +8527,9 @@ class AvatarCoreAdapter {
|
|
|
8204
8527
|
}
|
|
8205
8528
|
this.initializeMemoryViews();
|
|
8206
8529
|
}
|
|
8530
|
+
/**
|
|
8531
|
+
* 初始化内存视图
|
|
8532
|
+
*/
|
|
8207
8533
|
initializeMemoryViews() {
|
|
8208
8534
|
try {
|
|
8209
8535
|
const testPtr = this.wasmModule._malloc(4);
|
|
@@ -8222,32 +8548,57 @@ class AvatarCoreAdapter {
|
|
|
8222
8548
|
throw new Error(`Memory system initialization failed: ${errorMessage}`);
|
|
8223
8549
|
}
|
|
8224
8550
|
}
|
|
8551
|
+
/**
|
|
8552
|
+
* 设置 C API 函数包装
|
|
8553
|
+
*/
|
|
8225
8554
|
setupCAPIFunctions() {
|
|
8226
8555
|
this.api = {
|
|
8556
|
+
// 核心系统管理
|
|
8227
8557
|
initialize: this.wasmModule.cwrap("avatar_core_initialize", "number", ["number"]),
|
|
8558
|
+
// 返回 handle
|
|
8228
8559
|
release: this.wasmModule.cwrap("avatar_core_release", null, ["number"]),
|
|
8229
8560
|
getVersion: this.wasmModule.cwrap("avatar_core_get_version", "string", []),
|
|
8230
8561
|
getErrorString: this.wasmModule.cwrap("avatar_core_get_error_string", "string", ["number"]),
|
|
8231
8562
|
resetProcessorForReuse: this.wasmModule.cwrap("avatar_core_reset_processor_for_reuse", null, ["number"]),
|
|
8563
|
+
// 角色管理
|
|
8232
8564
|
loadCharacter: this.wasmModule.cwrap("avatar_core_load_character", "number", ["number", "number"]),
|
|
8565
|
+
// 返回 handle
|
|
8233
8566
|
removeCharacter: this.wasmModule.cwrap("avatar_core_remove_character", null, ["number", "number"]),
|
|
8234
8567
|
getCharacterInfo: this.wasmModule.cwrap("avatar_core_get_character_info", "number", ["number", "number", "number"]),
|
|
8235
8568
|
getCharacterShapeParams: this.wasmModule.cwrap("avatar_core_get_character_shape_params", "number", ["number", "number"]),
|
|
8569
|
+
// characterHandle, outputPtr
|
|
8570
|
+
// 动画管理
|
|
8236
8571
|
loadAnimation: this.wasmModule.cwrap("avatar_core_load_animation", "number", ["number", "number"]),
|
|
8237
8572
|
parseAnimationFramesFromFile: this.wasmModule.cwrap("avatar_core_parse_animation_frames_from_file", "number", ["number", "number", "number"]),
|
|
8238
8573
|
getAnimationFrameCount: this.wasmModule.cwrap("avatar_core_get_animation_frame_count", "number", ["number", "number"]),
|
|
8239
8574
|
getFrameFromAnimation: this.wasmModule.cwrap("avatar_core_get_frame_from_animation", "number", ["number", "number", "number"]),
|
|
8240
8575
|
removeAnimation: this.wasmModule.cwrap("avatar_core_remove_animation", null, ["number", "number"]),
|
|
8576
|
+
// 帧计算 (Flat 格式 - GPU 优化版本,协方差预计算)
|
|
8241
8577
|
computeFrameFlat: this.wasmModule.cwrap("avatar_core_compute_frame_as_splat_points_flat", "number", ["number", "number", "number", "number"]),
|
|
8578
|
+
// 返回错误码
|
|
8242
8579
|
freeSplatPointsFlat: this.wasmModule.cwrap("avatar_core_free_splat_points_flat", null, ["number"]),
|
|
8580
|
+
// Mesh 计算(用于传统 mesh 渲染)
|
|
8243
8581
|
computeFrameAsMesh: this.wasmModule.cwrap("avatar_core_compute_frame_as_mesh", "number", ["number", "number", "number", "number"]),
|
|
8582
|
+
// 返回错误码
|
|
8244
8583
|
freeMeshData: this.wasmModule.cwrap("avatar_core_free_mesh_data", null, ["number"]),
|
|
8584
|
+
// 眼部追踪(高级功能)
|
|
8245
8585
|
setEyeTrackingConfig: this.wasmModule.cwrap("avatar_core_set_eye_tracking_config", "number", ["number", "number", "number"]),
|
|
8586
|
+
// core, character, config
|
|
8246
8587
|
setGazeTarget: this.wasmModule.cwrap("avatar_core_set_gaze_target", "number", ["number", "number", "number", "number", "number"]),
|
|
8588
|
+
// core, character, x, y, z
|
|
8247
8589
|
resetEyeTracking: this.wasmModule.cwrap("avatar_core_reset_eye_tracking", "number", ["number"]),
|
|
8590
|
+
// FLAME 信息查询
|
|
8248
8591
|
getFlameInfo: this.wasmModule.cwrap("avatar_core_get_flame_info", "number", ["number", "number", "number", "number"])
|
|
8249
8592
|
};
|
|
8250
8593
|
}
|
|
8594
|
+
/**
|
|
8595
|
+
* 读取当前动画帧的 FlameParams(用于过渡或调试)
|
|
8596
|
+
*/
|
|
8597
|
+
/**
|
|
8598
|
+
* Get current frame parameters
|
|
8599
|
+
* @param frameIndex Frame index
|
|
8600
|
+
* @param characterId Optional character ID for multi-character support
|
|
8601
|
+
*/
|
|
8251
8602
|
async getCurrentFrameParams(frameIndex = 0, characterId) {
|
|
8252
8603
|
const paramsPtr = await this.getAnimationFrameParams(frameIndex, characterId);
|
|
8253
8604
|
try {
|
|
@@ -8257,6 +8608,9 @@ class AvatarCoreAdapter {
|
|
|
8257
8608
|
this.wasmModule._free(paramsPtr);
|
|
8258
8609
|
}
|
|
8259
8610
|
}
|
|
8611
|
+
/**
|
|
8612
|
+
* 初始化 Avatar Core 核心
|
|
8613
|
+
*/
|
|
8260
8614
|
async initializeAvatarCore(templateResources) {
|
|
8261
8615
|
try {
|
|
8262
8616
|
const configPtr = this.memoryManager.createTemplateConfig(templateResources);
|
|
@@ -8271,6 +8625,9 @@ class AvatarCoreAdapter {
|
|
|
8271
8625
|
throw new Error(`Failed to initialize Avatar Core: ${errorMessage}`);
|
|
8272
8626
|
}
|
|
8273
8627
|
}
|
|
8628
|
+
/**
|
|
8629
|
+
* 加载模板资源(从预加载的 ArrayBuffers)
|
|
8630
|
+
*/
|
|
8274
8631
|
async loadTemplateResourcesFromBuffers(templateResources) {
|
|
8275
8632
|
const formattedResources = {
|
|
8276
8633
|
flameModel: templateResources.flameModel || templateResources["model.pb"] || new ArrayBuffer(0),
|
|
@@ -8294,6 +8651,16 @@ class AvatarCoreAdapter {
|
|
|
8294
8651
|
throw new Error(`Template resources loading failed: ${errorMessage}`);
|
|
8295
8652
|
}
|
|
8296
8653
|
}
|
|
8654
|
+
/**
|
|
8655
|
+
* 加载角色数据(从预加载的 ArrayBuffers)
|
|
8656
|
+
*/
|
|
8657
|
+
/**
|
|
8658
|
+
* Load character from buffers and return handle
|
|
8659
|
+
* @param shapeBuffer Shape data buffer
|
|
8660
|
+
* @param pointCloudBuffer Point cloud data buffer
|
|
8661
|
+
* @param characterId Optional character ID for multi-character support
|
|
8662
|
+
* @returns Character handle
|
|
8663
|
+
*/
|
|
8297
8664
|
async loadCharacterFromBuffers(shapeBuffer, pointCloudBuffer, characterId) {
|
|
8298
8665
|
if (!this.isInitialized) {
|
|
8299
8666
|
throw new Error("Avatar Core not initialized");
|
|
@@ -8347,6 +8714,12 @@ class AvatarCoreAdapter {
|
|
|
8347
8714
|
}
|
|
8348
8715
|
return handle;
|
|
8349
8716
|
}
|
|
8717
|
+
/**
|
|
8718
|
+
* Load animation from ArrayBuffer (for CDN-based dynamic loading)
|
|
8719
|
+
* @param animData Animation data buffer
|
|
8720
|
+
* @param characterId Optional character ID for multi-character support
|
|
8721
|
+
* @returns Animation handle
|
|
8722
|
+
*/
|
|
8350
8723
|
async loadAnimationFromBuffer(animData, characterId) {
|
|
8351
8724
|
if (!this.isInitialized) {
|
|
8352
8725
|
throw new Error("Avatar Core not initialized");
|
|
@@ -8388,6 +8761,10 @@ class AvatarCoreAdapter {
|
|
|
8388
8761
|
throw new Error(`Failed to switch animation: ${errorMessage}`);
|
|
8389
8762
|
}
|
|
8390
8763
|
}
|
|
8764
|
+
/**
|
|
8765
|
+
* 获取动画总帧数
|
|
8766
|
+
* @param animationHandle Optional animation handle (for multi-character support)
|
|
8767
|
+
*/
|
|
8391
8768
|
async getAnimationTotalFrames(animationHandle) {
|
|
8392
8769
|
const handle = animationHandle || this.animationHandle;
|
|
8393
8770
|
if (!handle) {
|
|
@@ -8413,6 +8790,11 @@ class AvatarCoreAdapter {
|
|
|
8413
8790
|
throw new Error(`Failed to get animation frame count: ${errorMessage}`);
|
|
8414
8791
|
}
|
|
8415
8792
|
}
|
|
8793
|
+
/**
|
|
8794
|
+
* 从动画中获取指定帧的参数
|
|
8795
|
+
* @param frameIndex Frame index
|
|
8796
|
+
* @param characterId Optional character ID for multi-character support
|
|
8797
|
+
*/
|
|
8416
8798
|
async getAnimationFrameParams(frameIndex = 0, characterId) {
|
|
8417
8799
|
const id = characterId || "default";
|
|
8418
8800
|
const animInfo = this.animationHandles.get(id);
|
|
@@ -8449,6 +8831,11 @@ class AvatarCoreAdapter {
|
|
|
8449
8831
|
throw new Error(`Failed to get animation frame ${frameIndex}: ${errorMessage}`);
|
|
8450
8832
|
}
|
|
8451
8833
|
}
|
|
8834
|
+
/**
|
|
8835
|
+
* 设置眼部追踪配置(对齐 app 实现)
|
|
8836
|
+
* @param config Eye tracking configuration
|
|
8837
|
+
* @param characterId Optional character ID for multi-character support
|
|
8838
|
+
*/
|
|
8452
8839
|
async setEyeTrackingConfig(config, characterId) {
|
|
8453
8840
|
if (!this.isCharacterLoaded) {
|
|
8454
8841
|
throw new Error("Character not loaded");
|
|
@@ -8473,6 +8860,13 @@ class AvatarCoreAdapter {
|
|
|
8473
8860
|
throw new Error(`Failed to set eye tracking config: ${errorMessage}`);
|
|
8474
8861
|
}
|
|
8475
8862
|
}
|
|
8863
|
+
/**
|
|
8864
|
+
* 设置眼部追踪目标(高级功能)
|
|
8865
|
+
* @param x Target X coordinate
|
|
8866
|
+
* @param y Target Y coordinate
|
|
8867
|
+
* @param z Target Z coordinate
|
|
8868
|
+
* @param characterId Optional character ID for multi-character support
|
|
8869
|
+
*/
|
|
8476
8870
|
async setGazeTarget(x2, y2, z2, characterId) {
|
|
8477
8871
|
if (!this.isCharacterLoaded) {
|
|
8478
8872
|
throw new Error("Character not loaded");
|
|
@@ -8492,6 +8886,9 @@ class AvatarCoreAdapter {
|
|
|
8492
8886
|
throw new Error(`Failed to set gaze target: ${errorMessage}`);
|
|
8493
8887
|
}
|
|
8494
8888
|
}
|
|
8889
|
+
/**
|
|
8890
|
+
* 重置眼部追踪
|
|
8891
|
+
*/
|
|
8495
8892
|
async resetEyeTracking() {
|
|
8496
8893
|
if (!this.isCharacterLoaded) {
|
|
8497
8894
|
throw new Error("Character not loaded");
|
|
@@ -8506,6 +8903,9 @@ class AvatarCoreAdapter {
|
|
|
8506
8903
|
throw new Error(`Failed to reset eye tracking: ${errorMessage}`);
|
|
8507
8904
|
}
|
|
8508
8905
|
}
|
|
8906
|
+
/**
|
|
8907
|
+
* 查询 FLAME 模型信息
|
|
8908
|
+
*/
|
|
8509
8909
|
async queryFlameInfo() {
|
|
8510
8910
|
try {
|
|
8511
8911
|
const vertexCountPtr = this.wasmModule._malloc(4);
|
|
@@ -8530,6 +8930,9 @@ class AvatarCoreAdapter {
|
|
|
8530
8930
|
logger.errorWithError("Failed to query model info:", errorMessage);
|
|
8531
8931
|
}
|
|
8532
8932
|
}
|
|
8933
|
+
/**
|
|
8934
|
+
* 查询角色信息
|
|
8935
|
+
*/
|
|
8533
8936
|
async queryCharacterInfo() {
|
|
8534
8937
|
try {
|
|
8535
8938
|
const pointCountPtr = this.wasmModule._malloc(4);
|
|
@@ -8550,6 +8953,9 @@ class AvatarCoreAdapter {
|
|
|
8550
8953
|
logger.errorWithError("❌ Failed to query character info:", errorMessage);
|
|
8551
8954
|
}
|
|
8552
8955
|
}
|
|
8956
|
+
/**
|
|
8957
|
+
* 检查错误码并抛出异常
|
|
8958
|
+
*/
|
|
8553
8959
|
checkError(errorCode, functionName) {
|
|
8554
8960
|
if (errorCode !== 0) {
|
|
8555
8961
|
const errorName = this.errorCodes[errorCode] || `UNKNOWN_ERROR_${errorCode}`;
|
|
@@ -8558,11 +8964,17 @@ class AvatarCoreAdapter {
|
|
|
8558
8964
|
throw new Error(`${functionName} failed: ${errorName} - ${errorMessage}`);
|
|
8559
8965
|
}
|
|
8560
8966
|
}
|
|
8967
|
+
/**
|
|
8968
|
+
* 更新进度回调
|
|
8969
|
+
*/
|
|
8561
8970
|
async updateProgress(callback, message, progress) {
|
|
8562
8971
|
if (callback && typeof callback === "function") {
|
|
8563
8972
|
callback({ message, progress });
|
|
8564
8973
|
}
|
|
8565
8974
|
}
|
|
8975
|
+
/**
|
|
8976
|
+
* 获取性能指标
|
|
8977
|
+
*/
|
|
8566
8978
|
getPerformanceMetrics() {
|
|
8567
8979
|
var _a, _b;
|
|
8568
8980
|
return {
|
|
@@ -8572,6 +8984,11 @@ class AvatarCoreAdapter {
|
|
|
8572
8984
|
version: ((_b = (_a = this.api).getVersion) == null ? void 0 : _b.call(_a)) || "unknown"
|
|
8573
8985
|
};
|
|
8574
8986
|
}
|
|
8987
|
+
/**
|
|
8988
|
+
* 获取角色的点云数量
|
|
8989
|
+
* @param characterHandle 角色 handle(可选,默认使用当前角色)
|
|
8990
|
+
* @returns 点云数量,如果角色未加载则返回 null
|
|
8991
|
+
*/
|
|
8575
8992
|
getPointCount(characterHandle) {
|
|
8576
8993
|
if (!this.isCharacterLoaded) {
|
|
8577
8994
|
return null;
|
|
@@ -8598,6 +9015,14 @@ class AvatarCoreAdapter {
|
|
|
8598
9015
|
return null;
|
|
8599
9016
|
}
|
|
8600
9017
|
}
|
|
9018
|
+
/**
|
|
9019
|
+
* 释放当前角色和动画资源(但保留 core)
|
|
9020
|
+
*/
|
|
9021
|
+
/**
|
|
9022
|
+
* Remove a specific character by handle
|
|
9023
|
+
* @param characterHandle Character handle to remove
|
|
9024
|
+
* @param characterId Optional character ID for cleanup
|
|
9025
|
+
*/
|
|
8601
9026
|
removeCharacter(characterHandle, characterId) {
|
|
8602
9027
|
try {
|
|
8603
9028
|
if (!this.coreHandle) {
|
|
@@ -8621,11 +9046,18 @@ class AvatarCoreAdapter {
|
|
|
8621
9046
|
logger.errorWithError("❌ Failed to remove character:", error);
|
|
8622
9047
|
}
|
|
8623
9048
|
}
|
|
9049
|
+
/**
|
|
9050
|
+
* Release current character (legacy method, kept for backward compatibility)
|
|
9051
|
+
* @deprecated Use removeCharacter() instead for multi-character support
|
|
9052
|
+
*/
|
|
8624
9053
|
releaseCurrentCharacter() {
|
|
8625
9054
|
if (this.characterHandle) {
|
|
8626
9055
|
this.removeCharacter(this.characterHandle);
|
|
8627
9056
|
}
|
|
8628
9057
|
}
|
|
9058
|
+
/**
|
|
9059
|
+
* 释放所有资源(包括 core)
|
|
9060
|
+
*/
|
|
8629
9061
|
release() {
|
|
8630
9062
|
try {
|
|
8631
9063
|
this.releaseCurrentCharacter();
|
|
@@ -8648,12 +9080,31 @@ class AvatarCoreAdapter {
|
|
|
8648
9080
|
logger.errorWithError("❌ Failed to release resources:", error);
|
|
8649
9081
|
}
|
|
8650
9082
|
}
|
|
9083
|
+
/**
|
|
9084
|
+
* 兼容性接口:提供与 FlameComplete3DGSManager 相同的接口
|
|
9085
|
+
*/
|
|
9086
|
+
// 兼容 loadFlameModel
|
|
9087
|
+
// eslint-disable-next-line ts/no-explicit-any
|
|
8651
9088
|
async loadFlameModel(_modelData) {
|
|
8652
9089
|
return true;
|
|
8653
9090
|
}
|
|
9091
|
+
// 兼容 load3DGSData
|
|
9092
|
+
// eslint-disable-next-line ts/no-explicit-any
|
|
8654
9093
|
async load3DGSData(_original3DGSPoints, _binding, _flameFaces) {
|
|
8655
9094
|
return true;
|
|
8656
9095
|
}
|
|
9096
|
+
/**
|
|
9097
|
+
* 计算帧并返回 GPU 友好的 flat 格式(零拷贝,协方差预计算)
|
|
9098
|
+
* 🚀 性能优化版本:
|
|
9099
|
+
* - C++ 预计算协方差矩阵
|
|
9100
|
+
* - 零拷贝直接访问 WASM 内存http://localhost:3000/us/
|
|
9101
|
+
* - 直接输出 GPU 格式 [pos3, color4, cov6]
|
|
9102
|
+
*/
|
|
9103
|
+
/**
|
|
9104
|
+
* Compute complete frame from animation
|
|
9105
|
+
* @param params Frame parameters
|
|
9106
|
+
* @param characterHandle Optional character handle (for multi-character support)
|
|
9107
|
+
*/
|
|
8657
9108
|
async computeCompleteFrameFlat(params, characterHandle) {
|
|
8658
9109
|
if (!this.isCharacterLoaded) {
|
|
8659
9110
|
throw new Error("Character not loaded");
|
|
@@ -8709,6 +9160,15 @@ class AvatarCoreAdapter {
|
|
|
8709
9160
|
}
|
|
8710
9161
|
}
|
|
8711
9162
|
}
|
|
9163
|
+
/**
|
|
9164
|
+
* 计算帧并返回 GPU 友好的 flat 格式(从 FLAME 参数)
|
|
9165
|
+
* 🔑 用于 Realtime: 接受自定义 FLAME 参数并计算 Splat
|
|
9166
|
+
*/
|
|
9167
|
+
/**
|
|
9168
|
+
* Compute frame from Flame parameters
|
|
9169
|
+
* @param flameParams Flame parameters
|
|
9170
|
+
* @param characterHandle Optional character handle (for multi-character support)
|
|
9171
|
+
*/
|
|
8712
9172
|
async computeFrameFlatFromParams(flameParams, characterHandle) {
|
|
8713
9173
|
if (!this.isCharacterLoaded) {
|
|
8714
9174
|
throw new Error("Character not loaded");
|
|
@@ -8751,6 +9211,10 @@ class AvatarCoreAdapter {
|
|
|
8751
9211
|
}
|
|
8752
9212
|
}
|
|
8753
9213
|
}
|
|
9214
|
+
/**
|
|
9215
|
+
* 获取 WASM 内存使用情况(MB)
|
|
9216
|
+
* @returns WASM 内存使用量(MB),如果模块未加载则返回 null
|
|
9217
|
+
*/
|
|
8754
9218
|
getWASMMemoryMB() {
|
|
8755
9219
|
if (!this.wasmModule) {
|
|
8756
9220
|
return null;
|
|
@@ -8769,6 +9233,11 @@ class AvatarCoreAdapter {
|
|
|
8769
9233
|
}
|
|
8770
9234
|
}
|
|
8771
9235
|
class AvatarSDK {
|
|
9236
|
+
/**
|
|
9237
|
+
* Initialize SDK
|
|
9238
|
+
* @param appId Application ID to be included in both HTTP Headers and WebSocket Headers
|
|
9239
|
+
* @param configuration Configuration parameters
|
|
9240
|
+
*/
|
|
8772
9241
|
static async initialize(appId, configuration) {
|
|
8773
9242
|
var _a;
|
|
8774
9243
|
try {
|
|
@@ -8815,6 +9284,9 @@ class AvatarSDK {
|
|
|
8815
9284
|
throw error;
|
|
8816
9285
|
}
|
|
8817
9286
|
}
|
|
9287
|
+
/**
|
|
9288
|
+
* 初始化WASM模块(跟随整个SDK生命周期)
|
|
9289
|
+
*/
|
|
8818
9290
|
static async initializeWASMModule() {
|
|
8819
9291
|
try {
|
|
8820
9292
|
logger.log("[AvatarSDK] Initializing WASM module...");
|
|
@@ -8825,12 +9297,15 @@ class AvatarSDK {
|
|
|
8825
9297
|
baseAssetsPath: "",
|
|
8826
9298
|
wasmPath: "./avatar_core_wasm.js",
|
|
8827
9299
|
wasmConfig: {
|
|
9300
|
+
// Print function for debugging WASM output
|
|
8828
9301
|
print: (text) => {
|
|
8829
9302
|
logger.log(`[WASM] ${text}`);
|
|
8830
9303
|
},
|
|
8831
9304
|
printErr: (text) => {
|
|
8832
9305
|
logger.error(`[WASM ERROR] ${text}`);
|
|
8833
9306
|
},
|
|
9307
|
+
// 配置 locateFile 以确保能找到 WASM 文件
|
|
9308
|
+
// WASM 文件与 Emscripten JS 文件在同一目录(dist/ 根目录)
|
|
8834
9309
|
locateFile: (path, prefix) => {
|
|
8835
9310
|
if (path.startsWith("data:") || path.startsWith("http://") || path.startsWith("https://")) {
|
|
8836
9311
|
return path;
|
|
@@ -8846,6 +9321,10 @@ class AvatarSDK {
|
|
|
8846
9321
|
throw new Error(`WASM module initialization failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
8847
9322
|
}
|
|
8848
9323
|
}
|
|
9324
|
+
/**
|
|
9325
|
+
* 初始化模板资源(在SDK初始化时加载)
|
|
9326
|
+
* 从全局 CDN 加载模板资源,失败时直接抛出错误,阻止SDK初始化
|
|
9327
|
+
*/
|
|
8849
9328
|
static async initializeTemplateResources() {
|
|
8850
9329
|
if (!this._avatarCore) {
|
|
8851
9330
|
throw new Error("AvatarCore not available");
|
|
@@ -8871,12 +9350,22 @@ class AvatarSDK {
|
|
|
8871
9350
|
throw new Error(`SDK initialization failed: Template resources loading failed - ${errorMessage}`);
|
|
8872
9351
|
}
|
|
8873
9352
|
}
|
|
9353
|
+
/**
|
|
9354
|
+
* Set sessionToken
|
|
9355
|
+
* Developer Client -> Developer Server -> AvatarKit Server -> return sessionToken (max 1 hour validity)
|
|
9356
|
+
* Include in WebSocket Headers for avatar WebSocket service authentication
|
|
9357
|
+
*/
|
|
8874
9358
|
static setSessionToken(token) {
|
|
8875
9359
|
idManager.setSessionToken(token);
|
|
8876
9360
|
}
|
|
9361
|
+
/**
|
|
9362
|
+
* Set userId
|
|
9363
|
+
* Optional interface for developers, SDK includes this in telemetry logs
|
|
9364
|
+
*/
|
|
8877
9365
|
static setUserId(userId) {
|
|
8878
9366
|
idManager.setUserId(userId);
|
|
8879
9367
|
}
|
|
9368
|
+
// 只读属性
|
|
8880
9369
|
static get isInitialized() {
|
|
8881
9370
|
return this._isInitialized;
|
|
8882
9371
|
}
|
|
@@ -8895,17 +9384,32 @@ class AvatarSDK {
|
|
|
8895
9384
|
static get version() {
|
|
8896
9385
|
return this._version;
|
|
8897
9386
|
}
|
|
9387
|
+
/**
|
|
9388
|
+
* 获取播放模式(根据 drivingServiceMode)
|
|
9389
|
+
* @internal
|
|
9390
|
+
*/
|
|
8898
9391
|
static getPlaybackMode() {
|
|
8899
9392
|
var _a;
|
|
8900
9393
|
return ((_a = this._configuration) == null ? void 0 : _a.drivingServiceMode) ?? DrivingServiceMode.sdk;
|
|
8901
9394
|
}
|
|
9395
|
+
/**
|
|
9396
|
+
* 获取音频格式配置(带默认值)
|
|
9397
|
+
* @internal
|
|
9398
|
+
*/
|
|
8902
9399
|
static getAudioFormat() {
|
|
8903
9400
|
var _a;
|
|
8904
9401
|
return ((_a = this._configuration) == null ? void 0 : _a.audioFormat) ?? { channelCount: 1, sampleRate: 16e3 };
|
|
8905
9402
|
}
|
|
9403
|
+
/**
|
|
9404
|
+
* 获取AvatarCore实例(仅供 SDK 内部使用)
|
|
9405
|
+
* @internal
|
|
9406
|
+
*/
|
|
8906
9407
|
static getAvatarCore() {
|
|
8907
9408
|
return this._avatarCore;
|
|
8908
9409
|
}
|
|
9410
|
+
/**
|
|
9411
|
+
* Cleanup resources
|
|
9412
|
+
*/
|
|
8909
9413
|
static cleanup() {
|
|
8910
9414
|
if (!this._isInitialized) {
|
|
8911
9415
|
return;
|
|
@@ -8926,6 +9430,9 @@ class AvatarSDK {
|
|
|
8926
9430
|
logger.error("Failed to cleanup AvatarSDK:", error instanceof Error ? error.message : String(error));
|
|
8927
9431
|
}
|
|
8928
9432
|
}
|
|
9433
|
+
/**
|
|
9434
|
+
* 从远程配置接口获取SDK配置(已简化,逻辑移到 config/sdk-config-loader.ts)
|
|
9435
|
+
*/
|
|
8929
9436
|
static async _fetchSdkConfig() {
|
|
8930
9437
|
try {
|
|
8931
9438
|
this._dynamicSdkConfig = await fetchSdkConfig(this._version);
|
|
@@ -8937,6 +9444,10 @@ class AvatarSDK {
|
|
|
8937
9444
|
});
|
|
8938
9445
|
}
|
|
8939
9446
|
}
|
|
9447
|
+
/**
|
|
9448
|
+
* 获取环境配置(对齐 web 应用的 region-config)
|
|
9449
|
+
* @internal
|
|
9450
|
+
*/
|
|
8940
9451
|
static getEnvironmentConfig() {
|
|
8941
9452
|
if (!this._configuration) {
|
|
8942
9453
|
throw new Error("AvatarSDK not initialized");
|
|
@@ -8961,10 +9472,10 @@ class AvatarSDK {
|
|
|
8961
9472
|
}
|
|
8962
9473
|
__publicField(AvatarSDK, "_isInitialized", false);
|
|
8963
9474
|
__publicField(AvatarSDK, "_configuration", null);
|
|
8964
|
-
__publicField(AvatarSDK, "_version", "1.0.0-beta.
|
|
9475
|
+
__publicField(AvatarSDK, "_version", "1.0.0-beta.69");
|
|
8965
9476
|
__publicField(AvatarSDK, "_avatarCore", null);
|
|
8966
9477
|
__publicField(AvatarSDK, "_dynamicSdkConfig", null);
|
|
8967
|
-
const AvatarSDK$1 = Object.freeze(Object.defineProperty({
|
|
9478
|
+
const AvatarSDK$1 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
8968
9479
|
__proto__: null,
|
|
8969
9480
|
AvatarSDK
|
|
8970
9481
|
}, Symbol.toStringTag, { value: "Module" }));
|
|
@@ -9936,6 +10447,7 @@ class EventEmitter {
|
|
|
9936
10447
|
}
|
|
9937
10448
|
}
|
|
9938
10449
|
}
|
|
10450
|
+
// eslint-disable-next-line ts/no-explicit-any
|
|
9939
10451
|
emit(event, ...args) {
|
|
9940
10452
|
const handlers = this.events.get(event);
|
|
9941
10453
|
if (handlers) {
|
|
@@ -9951,6 +10463,7 @@ class EventEmitter {
|
|
|
9951
10463
|
}
|
|
9952
10464
|
}
|
|
9953
10465
|
class AnimationWebSocketClient extends EventEmitter {
|
|
10466
|
+
// v2 协议:标记会话是否已配置
|
|
9954
10467
|
constructor(options) {
|
|
9955
10468
|
super();
|
|
9956
10469
|
__publicField(this, "wsUrl");
|
|
@@ -9971,6 +10484,9 @@ class AnimationWebSocketClient extends EventEmitter {
|
|
|
9971
10484
|
this.appId = options.appId;
|
|
9972
10485
|
this.clientId = options.clientId;
|
|
9973
10486
|
}
|
|
10487
|
+
/**
|
|
10488
|
+
* 连接WebSocket
|
|
10489
|
+
*/
|
|
9974
10490
|
async connect(characterId) {
|
|
9975
10491
|
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
9976
10492
|
logger.log("[AnimationWebSocketClient] Already connected");
|
|
@@ -9996,6 +10512,9 @@ class AnimationWebSocketClient extends EventEmitter {
|
|
|
9996
10512
|
throw error;
|
|
9997
10513
|
}
|
|
9998
10514
|
}
|
|
10515
|
+
/**
|
|
10516
|
+
* 断开连接
|
|
10517
|
+
*/
|
|
9999
10518
|
disconnect() {
|
|
10000
10519
|
if (this.ws) {
|
|
10001
10520
|
this.ws.close(1e3, "Normal closure");
|
|
@@ -10013,6 +10532,10 @@ class AnimationWebSocketClient extends EventEmitter {
|
|
|
10013
10532
|
}
|
|
10014
10533
|
logger.log("[AnimationWebSocketClient] Disconnected");
|
|
10015
10534
|
}
|
|
10535
|
+
/**
|
|
10536
|
+
* 发送音频数据
|
|
10537
|
+
* @param conversationId - 会话ID(在 protobuf 协议中映射为 reqId 字段)
|
|
10538
|
+
*/
|
|
10016
10539
|
sendAudioData(conversationId, audioData, end) {
|
|
10017
10540
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
10018
10541
|
logger.error("[AnimationWebSocketClient] WebSocket not connected");
|
|
@@ -10048,15 +10571,26 @@ class AnimationWebSocketClient extends EventEmitter {
|
|
|
10048
10571
|
return false;
|
|
10049
10572
|
}
|
|
10050
10573
|
}
|
|
10574
|
+
/**
|
|
10575
|
+
* 生成会话ID
|
|
10576
|
+
* 使用统一的会话ID生成规则:YYYYMMDDHHmmss_nanoid
|
|
10577
|
+
*/
|
|
10051
10578
|
generateConversationId() {
|
|
10052
10579
|
return idManager.generateNewConversationId();
|
|
10053
10580
|
}
|
|
10581
|
+
/**
|
|
10582
|
+
* 获取连接状态
|
|
10583
|
+
*/
|
|
10054
10584
|
isConnected() {
|
|
10055
10585
|
return this.ws !== null && this.ws.readyState === WebSocket.OPEN;
|
|
10056
10586
|
}
|
|
10587
|
+
/**
|
|
10588
|
+
* 获取当前角色ID
|
|
10589
|
+
*/
|
|
10057
10590
|
getCurrentCharacterId() {
|
|
10058
10591
|
return this.currentCharacterId;
|
|
10059
10592
|
}
|
|
10593
|
+
// ========== 私有方法 ==========
|
|
10060
10594
|
buildWebSocketUrl(characterId) {
|
|
10061
10595
|
const url = new URL(this.wsUrl);
|
|
10062
10596
|
url.searchParams.set("id", characterId);
|
|
@@ -10183,6 +10717,9 @@ class AnimationWebSocketClient extends EventEmitter {
|
|
|
10183
10717
|
}
|
|
10184
10718
|
});
|
|
10185
10719
|
}
|
|
10720
|
+
/**
|
|
10721
|
+
* 清理 URL 用于日志记录(隐藏敏感信息)
|
|
10722
|
+
*/
|
|
10186
10723
|
sanitizeUrlForLog(url) {
|
|
10187
10724
|
try {
|
|
10188
10725
|
const urlObj = new URL(url);
|
|
@@ -10199,6 +10736,9 @@ class AnimationWebSocketClient extends EventEmitter {
|
|
|
10199
10736
|
return url.length > 100 ? `${url.substring(0, 100)}...` : url;
|
|
10200
10737
|
}
|
|
10201
10738
|
}
|
|
10739
|
+
/**
|
|
10740
|
+
* v2 协议:配置会话(发送采样率等参数)
|
|
10741
|
+
*/
|
|
10202
10742
|
configureSession() {
|
|
10203
10743
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
10204
10744
|
logger.error("[AnimationWebSocketClient] Cannot configure session: WebSocket not open");
|
|
@@ -10211,6 +10751,7 @@ class AnimationWebSocketClient extends EventEmitter {
|
|
|
10211
10751
|
clientConfigureSession: {
|
|
10212
10752
|
sampleRate: audioFormatConfig.sampleRate,
|
|
10213
10753
|
bitrate: 0,
|
|
10754
|
+
// 根据实际需求设置
|
|
10214
10755
|
audioFormat: AudioFormat.AUDIO_FORMAT_PCM_S16LE,
|
|
10215
10756
|
transportCompression: TransportCompression.TRANSPORT_COMPRESSION_NONE
|
|
10216
10757
|
}
|
|
@@ -10285,13 +10826,18 @@ class NetworkLayer {
|
|
|
10285
10826
|
constructor(dataController) {
|
|
10286
10827
|
__publicField(this, "wsClient");
|
|
10287
10828
|
__publicField(this, "dataController");
|
|
10829
|
+
// 组合播放层
|
|
10288
10830
|
__publicField(this, "currentConversationId", null);
|
|
10289
10831
|
__publicField(this, "audioMetrics", this.createAudioMetrics());
|
|
10290
10832
|
__publicField(this, "isFallbackMode", false);
|
|
10833
|
+
// 连接超时降级模式标记
|
|
10291
10834
|
__publicField(this, "isConnecting", false);
|
|
10835
|
+
// 避免并发连接
|
|
10836
|
+
// 驱动服务心跳检测相关
|
|
10292
10837
|
__publicField(this, "heartbeatTimer", null);
|
|
10293
10838
|
__publicField(this, "visibilityChangeHandler", null);
|
|
10294
10839
|
__publicField(this, "heartbeatFailureCount", 0);
|
|
10840
|
+
// 心跳失败计数
|
|
10295
10841
|
__publicField(this, "HEARTBEAT_INTERVAL", 12e4);
|
|
10296
10842
|
this.dataController = dataController;
|
|
10297
10843
|
const config = AvatarSDK.getEnvironmentConfig();
|
|
@@ -10305,10 +10851,17 @@ class NetworkLayer {
|
|
|
10305
10851
|
this.setupWebSocketListeners();
|
|
10306
10852
|
this.startHeartbeatCheck();
|
|
10307
10853
|
}
|
|
10854
|
+
// 每2分钟检查一次
|
|
10855
|
+
/**
|
|
10856
|
+
* 获取音频字节每秒(根据配置的采样率动态计算)
|
|
10857
|
+
*/
|
|
10308
10858
|
getAudioBytesPerSecond() {
|
|
10309
10859
|
const audioFormat = AvatarSDK.getAudioFormat();
|
|
10310
10860
|
return audioFormat.sampleRate * 2;
|
|
10311
10861
|
}
|
|
10862
|
+
/**
|
|
10863
|
+
* 连接服务
|
|
10864
|
+
*/
|
|
10312
10865
|
async connect(characterId) {
|
|
10313
10866
|
var _a, _b, _c, _d;
|
|
10314
10867
|
if (this.isConnecting) {
|
|
@@ -10352,9 +10905,17 @@ class NetworkLayer {
|
|
|
10352
10905
|
this.isConnecting = false;
|
|
10353
10906
|
}
|
|
10354
10907
|
}
|
|
10908
|
+
/**
|
|
10909
|
+
* 检查是否可以发送音频数据(包括降级模式)
|
|
10910
|
+
* @internal
|
|
10911
|
+
*/
|
|
10355
10912
|
canSend() {
|
|
10356
10913
|
return this.isFallbackMode || this.dataController.connected;
|
|
10357
10914
|
}
|
|
10915
|
+
/**
|
|
10916
|
+
* 发送音频数据到服务器
|
|
10917
|
+
* 注意:打断逻辑由 AvatarController.send() 统一处理,这里只负责网络通信
|
|
10918
|
+
*/
|
|
10358
10919
|
sendAudioData(audioData, isLast) {
|
|
10359
10920
|
var _a, _b;
|
|
10360
10921
|
if (!this.currentConversationId) {
|
|
@@ -10400,6 +10961,9 @@ class NetworkLayer {
|
|
|
10400
10961
|
return;
|
|
10401
10962
|
}
|
|
10402
10963
|
}
|
|
10964
|
+
/**
|
|
10965
|
+
* 断开连接
|
|
10966
|
+
*/
|
|
10403
10967
|
disconnect() {
|
|
10404
10968
|
this.isFallbackMode = false;
|
|
10405
10969
|
this.isConnecting = false;
|
|
@@ -10409,13 +10973,23 @@ class NetworkLayer {
|
|
|
10409
10973
|
idManager.clearConnectionId();
|
|
10410
10974
|
this.currentConversationId = null;
|
|
10411
10975
|
}
|
|
10976
|
+
/**
|
|
10977
|
+
* 获取当前会话ID
|
|
10978
|
+
*/
|
|
10412
10979
|
getCurrentConversationId() {
|
|
10413
10980
|
return this.currentConversationId;
|
|
10414
10981
|
}
|
|
10982
|
+
/**
|
|
10983
|
+
* 重置会话ID(用于打断后清理)
|
|
10984
|
+
*/
|
|
10415
10985
|
resetConversationId() {
|
|
10416
10986
|
this.currentConversationId = null;
|
|
10417
10987
|
this.resetAudioMetrics();
|
|
10418
10988
|
}
|
|
10989
|
+
/**
|
|
10990
|
+
* 设置 WebSocket 事件监听器
|
|
10991
|
+
* Safety: Remove existing listeners before adding new ones to prevent duplicates
|
|
10992
|
+
*/
|
|
10419
10993
|
setupWebSocketListeners() {
|
|
10420
10994
|
this.wsClient.removeAllListeners();
|
|
10421
10995
|
this.wsClient.on("sessionConfirmed", (connectionId) => {
|
|
@@ -10450,6 +11024,9 @@ class NetworkLayer {
|
|
|
10450
11024
|
this.handleMessage(message);
|
|
10451
11025
|
});
|
|
10452
11026
|
}
|
|
11027
|
+
/**
|
|
11028
|
+
* 处理接收到的消息
|
|
11029
|
+
*/
|
|
10453
11030
|
handleMessage(message) {
|
|
10454
11031
|
try {
|
|
10455
11032
|
switch (message.type) {
|
|
@@ -10465,6 +11042,9 @@ class NetworkLayer {
|
|
|
10465
11042
|
logger.error("[NetworkLayer] Failed to handle message:", message2);
|
|
10466
11043
|
}
|
|
10467
11044
|
}
|
|
11045
|
+
/**
|
|
11046
|
+
* 处理动画消息
|
|
11047
|
+
*/
|
|
10468
11048
|
handleAnimationMessage(message) {
|
|
10469
11049
|
if (!message.serverResponseAnimation) {
|
|
10470
11050
|
logger.error("[NetworkLayer] Invalid animation message");
|
|
@@ -10514,6 +11094,9 @@ class NetworkLayer {
|
|
|
10514
11094
|
});
|
|
10515
11095
|
}
|
|
10516
11096
|
}
|
|
11097
|
+
/**
|
|
11098
|
+
* 处理错误消息
|
|
11099
|
+
*/
|
|
10517
11100
|
handleErrorMessage(message) {
|
|
10518
11101
|
var _a, _b;
|
|
10519
11102
|
if (!message.serverError) {
|
|
@@ -10558,6 +11141,9 @@ class NetworkLayer {
|
|
|
10558
11141
|
this.dataController.yieldKeyframes([], conversationId);
|
|
10559
11142
|
}
|
|
10560
11143
|
}
|
|
11144
|
+
/**
|
|
11145
|
+
* 创建音频指标
|
|
11146
|
+
*/
|
|
10561
11147
|
createAudioMetrics() {
|
|
10562
11148
|
return {
|
|
10563
11149
|
accumulatedBytes: 0,
|
|
@@ -10569,9 +11155,16 @@ class NetworkLayer {
|
|
|
10569
11155
|
didReportLatency: false
|
|
10570
11156
|
};
|
|
10571
11157
|
}
|
|
11158
|
+
/**
|
|
11159
|
+
* 重置音频指标
|
|
11160
|
+
*/
|
|
10572
11161
|
resetAudioMetrics() {
|
|
10573
11162
|
this.audioMetrics = this.createAudioMetrics();
|
|
10574
11163
|
}
|
|
11164
|
+
/**
|
|
11165
|
+
* 上报 driving_service_latency 事件
|
|
11166
|
+
* 每轮会话只上报一次(在收到首帧时)
|
|
11167
|
+
*/
|
|
10575
11168
|
reportDrivingServiceLatency(conversationId) {
|
|
10576
11169
|
if (!conversationId) {
|
|
10577
11170
|
return;
|
|
@@ -10580,12 +11173,21 @@ class NetworkLayer {
|
|
|
10580
11173
|
logEvent("driving_service_latency", "info", {
|
|
10581
11174
|
req_id: conversationId,
|
|
10582
11175
|
dsm: "sdk",
|
|
11176
|
+
// 写死 sdk
|
|
10583
11177
|
tap_0: metrics.startTimestamp || 0,
|
|
11178
|
+
// 必须有非零值
|
|
10584
11179
|
tap_1: metrics.tap1Timestamp || 0,
|
|
11180
|
+
// 缺省值为 0
|
|
10585
11181
|
tap_2: metrics.tap2Timestamp || 0,
|
|
11182
|
+
// 缺省值为 0
|
|
10586
11183
|
tap_f: metrics.recvFirstFlameTimestamp || 0
|
|
11184
|
+
// 必须有非零值
|
|
10587
11185
|
});
|
|
10588
11186
|
}
|
|
11187
|
+
/**
|
|
11188
|
+
* 启动驱动服务心跳检测(每2分钟检查一次连接状态)
|
|
11189
|
+
* @private
|
|
11190
|
+
*/
|
|
10589
11191
|
startHeartbeatCheck() {
|
|
10590
11192
|
this.stopHeartbeatCheck();
|
|
10591
11193
|
this.heartbeatTimer = window.setInterval(() => {
|
|
@@ -10595,6 +11197,10 @@ class NetworkLayer {
|
|
|
10595
11197
|
}, this.HEARTBEAT_INTERVAL);
|
|
10596
11198
|
this.setupVisibilityListener();
|
|
10597
11199
|
}
|
|
11200
|
+
/**
|
|
11201
|
+
* 停止驱动服务心跳检测
|
|
11202
|
+
* @private
|
|
11203
|
+
*/
|
|
10598
11204
|
stopHeartbeatCheck() {
|
|
10599
11205
|
if (this.heartbeatTimer !== null) {
|
|
10600
11206
|
clearInterval(this.heartbeatTimer);
|
|
@@ -10603,6 +11209,11 @@ class NetworkLayer {
|
|
|
10603
11209
|
this.removeVisibilityListener();
|
|
10604
11210
|
this.heartbeatFailureCount = 0;
|
|
10605
11211
|
}
|
|
11212
|
+
/**
|
|
11213
|
+
* 执行心跳检查:检测 WebSocket 连接状态
|
|
11214
|
+
* 如果连接失败 3 次,上报 heartbeat_failed 事件
|
|
11215
|
+
* @private
|
|
11216
|
+
*/
|
|
10606
11217
|
performHeartbeatCheck() {
|
|
10607
11218
|
try {
|
|
10608
11219
|
const isConnected = this.wsClient.isConnected();
|
|
@@ -10631,6 +11242,10 @@ class NetworkLayer {
|
|
|
10631
11242
|
}
|
|
10632
11243
|
}
|
|
10633
11244
|
}
|
|
11245
|
+
/**
|
|
11246
|
+
* 设置页面可见性监听器(用于页面重新可见时立即执行心跳检查)
|
|
11247
|
+
* @private
|
|
11248
|
+
*/
|
|
10634
11249
|
setupVisibilityListener() {
|
|
10635
11250
|
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
10636
11251
|
return;
|
|
@@ -10643,6 +11258,10 @@ class NetworkLayer {
|
|
|
10643
11258
|
};
|
|
10644
11259
|
document.addEventListener("visibilitychange", this.visibilityChangeHandler);
|
|
10645
11260
|
}
|
|
11261
|
+
/**
|
|
11262
|
+
* 移除页面可见性监听器
|
|
11263
|
+
* @private
|
|
11264
|
+
*/
|
|
10646
11265
|
removeVisibilityListener() {
|
|
10647
11266
|
if (typeof window === "undefined" || typeof document === "undefined" || !this.visibilityChangeHandler) {
|
|
10648
11267
|
return;
|
|
@@ -10652,35 +11271,56 @@ class NetworkLayer {
|
|
|
10652
11271
|
}
|
|
10653
11272
|
}
|
|
10654
11273
|
class AvatarController {
|
|
11274
|
+
// 16kHz * 2 bytes per sample
|
|
10655
11275
|
constructor(avatar, options) {
|
|
11276
|
+
// ========== Configuration and Composition ==========
|
|
10656
11277
|
__publicField(this, "networkLayer");
|
|
10657
11278
|
__publicField(this, "playbackMode");
|
|
10658
11279
|
__publicField(this, "avatar");
|
|
11280
|
+
// ========== Player Management ==========
|
|
10659
11281
|
__publicField(this, "animationPlayer", null);
|
|
10660
11282
|
__publicField(this, "currentKeyframes", []);
|
|
10661
11283
|
__publicField(this, "pendingAudioChunks", []);
|
|
10662
11284
|
__publicField(this, "isPlaying", false);
|
|
10663
11285
|
__publicField(this, "isStartingPlayback", false);
|
|
11286
|
+
// 防止重复调用 startStreamingPlayback
|
|
11287
|
+
// ========== State Management ==========
|
|
10664
11288
|
__publicField(this, "isConnected", false);
|
|
10665
11289
|
__publicField(this, "currentState", AvatarState.idle);
|
|
11290
|
+
// ========== Conversation ID Management (for host mode) ==========
|
|
10666
11291
|
__publicField(this, "currentConversationId", null);
|
|
10667
11292
|
__publicField(this, "reqEnd", false);
|
|
11293
|
+
// ========== Event System ==========
|
|
10668
11294
|
__publicField(this, "onConnectionState", null);
|
|
10669
11295
|
__publicField(this, "onConversationState", null);
|
|
10670
11296
|
__publicField(this, "onError", null);
|
|
10671
11297
|
__publicField(this, "eventListeners", /* @__PURE__ */ new Map());
|
|
11298
|
+
// ========== Callbacks ==========
|
|
10672
11299
|
__publicField(this, "renderCallback");
|
|
10673
11300
|
__publicField(this, "characterHandle", null);
|
|
11301
|
+
// Character handle for multi-character support
|
|
10674
11302
|
__publicField(this, "characterId", null);
|
|
11303
|
+
// Character ID for multi-character support (used for eye tracking)
|
|
11304
|
+
// ========== Post-processing Configuration ==========
|
|
10675
11305
|
__publicField(this, "postProcessingConfig", null);
|
|
11306
|
+
// ========== Playback Loop ==========
|
|
10676
11307
|
__publicField(this, "playbackLoopId", null);
|
|
10677
11308
|
__publicField(this, "lastRenderedFrameIndex", -1);
|
|
10678
11309
|
__publicField(this, "keyframesOffset", 0);
|
|
11310
|
+
// Offset to track how many frames were removed from the beginning
|
|
10679
11311
|
__publicField(this, "MAX_KEYFRAMES", 5e3);
|
|
11312
|
+
// Maximum keyframes to keep in memory during playback
|
|
10680
11313
|
__publicField(this, "KEYFRAMES_CLEANUP_THRESHOLD", 3e3);
|
|
11314
|
+
// Cleanup threshold (keep some buffer)
|
|
11315
|
+
// 日志控制:避免刷屏
|
|
10681
11316
|
__publicField(this, "lastSyncLogTime", 0);
|
|
11317
|
+
// 上次同步状态日志时间
|
|
10682
11318
|
__publicField(this, "lastOutOfBoundsState", false);
|
|
11319
|
+
// 上次是否超出范围的状态
|
|
11320
|
+
// ========== Audio Only Mode ==========
|
|
10683
11321
|
__publicField(this, "isAudioOnlyMode", false);
|
|
11322
|
+
// 音频独立播放模式标志
|
|
11323
|
+
// ========== Playback Stuck Detection ==========
|
|
10684
11324
|
__publicField(this, "playbackStuckCheckState", {
|
|
10685
11325
|
audioTimeZeroCount: 0,
|
|
10686
11326
|
lastAudioTime: 0,
|
|
@@ -10688,8 +11328,12 @@ class AvatarController {
|
|
|
10688
11328
|
reported: false
|
|
10689
11329
|
});
|
|
10690
11330
|
__publicField(this, "MAX_AUDIO_TIME_ZERO_COUNT", 60);
|
|
11331
|
+
// 约 1 秒(60fps)
|
|
10691
11332
|
__publicField(this, "MAX_AUDIO_TIME_STUCK_COUNT", 60);
|
|
11333
|
+
// 约 1 秒(60fps)
|
|
10692
11334
|
__publicField(this, "AUDIO_TIME_STUCK_THRESHOLD", 1e-3);
|
|
11335
|
+
// 1ms,小于此值视为卡住
|
|
11336
|
+
// ========== Host Mode Latency Metrics ==========
|
|
10693
11337
|
__publicField(this, "hostModeMetrics", {
|
|
10694
11338
|
accumulatedBytes: 0,
|
|
10695
11339
|
startTimestamp: 0,
|
|
@@ -10706,21 +11350,46 @@ class AvatarController {
|
|
|
10706
11350
|
this.networkLayer = new NetworkLayer(this);
|
|
10707
11351
|
}
|
|
10708
11352
|
}
|
|
11353
|
+
// ========== Internal Accessors (for NetworkLayer and AvatarView) ==========
|
|
11354
|
+
/**
|
|
11355
|
+
* Get Avatar ID (for NetworkLayer use)
|
|
11356
|
+
* @internal
|
|
11357
|
+
*/
|
|
10709
11358
|
getAvatarId() {
|
|
10710
11359
|
return this.avatar.id;
|
|
10711
11360
|
}
|
|
11361
|
+
/**
|
|
11362
|
+
* Get playback state (for NetworkLayer use)
|
|
11363
|
+
* @internal
|
|
11364
|
+
*/
|
|
10712
11365
|
getIsPlaying() {
|
|
10713
11366
|
return this.isPlaying;
|
|
10714
11367
|
}
|
|
11368
|
+
/**
|
|
11369
|
+
* Set connection state (for NetworkLayer use)
|
|
11370
|
+
* @internal
|
|
11371
|
+
*/
|
|
10715
11372
|
setConnected(connected) {
|
|
10716
11373
|
this.isConnected = connected;
|
|
10717
11374
|
}
|
|
11375
|
+
/**
|
|
11376
|
+
* Get connection state
|
|
11377
|
+
* @internal
|
|
11378
|
+
*/
|
|
10718
11379
|
get connected() {
|
|
10719
11380
|
return this.isConnected;
|
|
10720
11381
|
}
|
|
11382
|
+
/**
|
|
11383
|
+
* Get current state
|
|
11384
|
+
* @internal
|
|
11385
|
+
*/
|
|
10721
11386
|
get state() {
|
|
10722
11387
|
return this.currentState;
|
|
10723
11388
|
}
|
|
11389
|
+
/**
|
|
11390
|
+
* 将内部 AvatarState 映射到外部 ConversationState
|
|
11391
|
+
* @internal
|
|
11392
|
+
*/
|
|
10724
11393
|
mapToConversationState(avatarState) {
|
|
10725
11394
|
switch (avatarState) {
|
|
10726
11395
|
case AvatarState.idle:
|
|
@@ -10735,12 +11404,35 @@ class AvatarController {
|
|
|
10735
11404
|
return ConversationState.idle;
|
|
10736
11405
|
}
|
|
10737
11406
|
}
|
|
11407
|
+
/**
|
|
11408
|
+
* Get animation player instance
|
|
11409
|
+
* @internal
|
|
11410
|
+
*/
|
|
10738
11411
|
getAnimationPlayer() {
|
|
10739
11412
|
return this.animationPlayer;
|
|
10740
11413
|
}
|
|
11414
|
+
/**
|
|
11415
|
+
* Get current conversation ID
|
|
11416
|
+
* Returns the current conversation ID for the active audio session
|
|
11417
|
+
* @returns Current conversation ID, or null if no active session
|
|
11418
|
+
*/
|
|
10741
11419
|
getCurrentConversationId() {
|
|
10742
11420
|
return this.getEffectiveConversationId();
|
|
10743
11421
|
}
|
|
11422
|
+
// ========== Audio Context Initialization ==========
|
|
11423
|
+
/**
|
|
11424
|
+
* Initialize audio context (must be called in user gesture context)
|
|
11425
|
+
*
|
|
11426
|
+
* This method must be called before any audio operations (send, yieldAudioData, etc.)
|
|
11427
|
+
* to ensure AudioContext is created and initialized in a user gesture context.
|
|
11428
|
+
*
|
|
11429
|
+
* @example
|
|
11430
|
+
* // In user click handler
|
|
11431
|
+
* button.addEventListener('click', async () => {
|
|
11432
|
+
* await avatarView.controller.initializeAudioContext()
|
|
11433
|
+
* // Now you can safely use send() or yieldAudioData()
|
|
11434
|
+
* })
|
|
11435
|
+
*/
|
|
10744
11436
|
async initializeAudioContext() {
|
|
10745
11437
|
var _a;
|
|
10746
11438
|
if ((_a = this.animationPlayer) == null ? void 0 : _a.isStreamingReady()) {
|
|
@@ -10773,6 +11465,10 @@ class AvatarController {
|
|
|
10773
11465
|
}
|
|
10774
11466
|
}
|
|
10775
11467
|
}
|
|
11468
|
+
/**
|
|
11469
|
+
* Check if audio context is initialized
|
|
11470
|
+
* @throws AvatarError if audio context is not initialized
|
|
11471
|
+
*/
|
|
10776
11472
|
checkAudioContextInitialized() {
|
|
10777
11473
|
var _a;
|
|
10778
11474
|
if (!((_a = this.animationPlayer) == null ? void 0 : _a.isStreamingReady())) {
|
|
@@ -10782,6 +11478,10 @@ class AvatarController {
|
|
|
10782
11478
|
);
|
|
10783
11479
|
}
|
|
10784
11480
|
}
|
|
11481
|
+
// ========== SDK Mode Interface ==========
|
|
11482
|
+
/**
|
|
11483
|
+
* Start service (SDK mode only)
|
|
11484
|
+
*/
|
|
10785
11485
|
async start() {
|
|
10786
11486
|
if (!this.networkLayer) {
|
|
10787
11487
|
throw new AvatarError(
|
|
@@ -10792,6 +11492,11 @@ class AvatarController {
|
|
|
10792
11492
|
this.checkAudioContextInitialized();
|
|
10793
11493
|
await this.networkLayer.connect(this.avatar.id);
|
|
10794
11494
|
}
|
|
11495
|
+
/**
|
|
11496
|
+
* Send audio to server (SDK mode only)
|
|
11497
|
+
* Also cache to data layer for playback
|
|
11498
|
+
* @returns conversationId - Conversation ID for this audio session
|
|
11499
|
+
*/
|
|
10795
11500
|
send(audioData, end = false) {
|
|
10796
11501
|
var _a, _b, _c, _d;
|
|
10797
11502
|
try {
|
|
@@ -10827,6 +11532,9 @@ class AvatarController {
|
|
|
10827
11532
|
}
|
|
10828
11533
|
return this.networkLayer.getCurrentConversationId();
|
|
10829
11534
|
}
|
|
11535
|
+
/**
|
|
11536
|
+
* Close service (SDK mode only)
|
|
11537
|
+
*/
|
|
10830
11538
|
close() {
|
|
10831
11539
|
var _a;
|
|
10832
11540
|
if (this.isPlaying || this.currentState === AvatarState.paused) {
|
|
@@ -10842,6 +11550,15 @@ class AvatarController {
|
|
|
10842
11550
|
this.isConnected = false;
|
|
10843
11551
|
(_a = this.onConnectionState) == null ? void 0 : _a.call(this, ConnectionState.disconnected);
|
|
10844
11552
|
}
|
|
11553
|
+
// ========== Host Mode Interface ==========
|
|
11554
|
+
/**
|
|
11555
|
+
* Playback existing audio and animation data (host mode)
|
|
11556
|
+
* Starts a new conversation by generating a new conversation ID and interrupting any existing conversation
|
|
11557
|
+
* @param initialAudioChunks - Existing audio chunks to playback
|
|
11558
|
+
* @param initialKeyframes - Existing animation keyframes to playback
|
|
11559
|
+
* @returns conversationId - New conversation ID for this conversation session
|
|
11560
|
+
* @internal
|
|
11561
|
+
*/
|
|
10845
11562
|
async playback(initialAudioChunks, initialKeyframes) {
|
|
10846
11563
|
this.checkAudioContextInitialized();
|
|
10847
11564
|
if (this.isPlaying || this.currentConversationId) {
|
|
@@ -10875,6 +11592,11 @@ class AvatarController {
|
|
|
10875
11592
|
}
|
|
10876
11593
|
return this.currentConversationId;
|
|
10877
11594
|
}
|
|
11595
|
+
/**
|
|
11596
|
+
* Send audio data (host mode)
|
|
11597
|
+
* Stream additional audio data after playback()
|
|
11598
|
+
* @returns conversationId - Conversation ID for this audio session
|
|
11599
|
+
*/
|
|
10878
11600
|
yieldAudioData(data, isLast = false) {
|
|
10879
11601
|
var _a, _b, _c;
|
|
10880
11602
|
try {
|
|
@@ -10923,6 +11645,15 @@ class AvatarController {
|
|
|
10923
11645
|
}
|
|
10924
11646
|
return this.currentConversationId;
|
|
10925
11647
|
}
|
|
11648
|
+
/**
|
|
11649
|
+
* Send animation keyframes (host mode or SDK mode)
|
|
11650
|
+
* Stream additional animation data after playback()
|
|
11651
|
+
*
|
|
11652
|
+
* Public API: accepts binary data array (protobuf encoded Message array)
|
|
11653
|
+
* @param keyframesDataArray - Animation keyframes binary data array (each element is a protobuf encoded Message) or empty array to trigger audio-only mode
|
|
11654
|
+
* @param conversationId - Conversation ID (required). If conversationId doesn't match current conversationId, keyframes will be discarded.
|
|
11655
|
+
* Use getCurrentConversationId() to get the current conversationId.
|
|
11656
|
+
*/
|
|
10926
11657
|
yieldFramesData(keyframesDataArray, conversationId) {
|
|
10927
11658
|
var _a, _b;
|
|
10928
11659
|
if (!keyframesDataArray || keyframesDataArray.length === 0) {
|
|
@@ -10963,6 +11694,18 @@ class AvatarController {
|
|
|
10963
11694
|
}
|
|
10964
11695
|
this.yieldKeyframes(allKeyframes, conversationId);
|
|
10965
11696
|
}
|
|
11697
|
+
/**
|
|
11698
|
+
* Send animation keyframes (host mode)
|
|
11699
|
+
* Stream animation keyframes data after yieldAudioData()
|
|
11700
|
+
*
|
|
11701
|
+
* ⚠️ **Not recommended**: Prefer using `yieldFramesData()` method, which accepts binary data (protobuf encoded Message) with better performance.
|
|
11702
|
+
* This method is only for scenarios where you already have decoded KeyframeData arrays.
|
|
11703
|
+
*
|
|
11704
|
+
* Public API: accepts decoded keyframes
|
|
11705
|
+
* @param keyframes - Animation keyframes array (KeyframeData[]) or empty array to trigger audio-only mode
|
|
11706
|
+
* @param conversationId - Conversation ID (required). If conversationId doesn't match current conversationId, keyframes will be discarded.
|
|
11707
|
+
* Use getCurrentConversationId() to get the current conversationId.
|
|
11708
|
+
*/
|
|
10966
11709
|
yieldKeyframes(keyframes, conversationId) {
|
|
10967
11710
|
if (!conversationId || typeof conversationId !== "string") {
|
|
10968
11711
|
logger.error(`[AvatarController] yieldKeyframes requires a valid conversationId. Use getCurrentConversationId() to get the current conversationId.`);
|
|
@@ -11003,8 +11746,9 @@ class AvatarController {
|
|
|
11003
11746
|
this.enableAudioOnlyMode();
|
|
11004
11747
|
return;
|
|
11005
11748
|
}
|
|
11749
|
+
const flameKeyframes = keyframes;
|
|
11006
11750
|
if (this.currentKeyframes.length === 0) {
|
|
11007
|
-
this.currentKeyframes =
|
|
11751
|
+
this.currentKeyframes = flameKeyframes;
|
|
11008
11752
|
if (this.playbackMode === DrivingServiceMode.host && !this.hostModeMetrics.didRecvFirstFlame) {
|
|
11009
11753
|
this.hostModeMetrics.didRecvFirstFlame = true;
|
|
11010
11754
|
this.hostModeMetrics.recvFirstFlameTimestamp = Date.now();
|
|
@@ -11014,7 +11758,7 @@ class AvatarController {
|
|
|
11014
11758
|
}
|
|
11015
11759
|
}
|
|
11016
11760
|
} else {
|
|
11017
|
-
this.currentKeyframes.push(...
|
|
11761
|
+
this.currentKeyframes.push(...flameKeyframes);
|
|
11018
11762
|
}
|
|
11019
11763
|
this.emit("keyframesUpdate", this.currentKeyframes);
|
|
11020
11764
|
if (!this.isPlaying && !this.isStartingPlayback && this.pendingAudioChunks.length > 0 && this.currentKeyframes.length > 0) {
|
|
@@ -11026,6 +11770,11 @@ class AvatarController {
|
|
|
11026
11770
|
});
|
|
11027
11771
|
}
|
|
11028
11772
|
}
|
|
11773
|
+
// ========== Common Interface ==========
|
|
11774
|
+
/**
|
|
11775
|
+
* Pause playback (can be resumed later)
|
|
11776
|
+
* Pause audio playback and stop render loop, but preserve all state (keyframes, audio buffers, etc.)
|
|
11777
|
+
*/
|
|
11029
11778
|
pause() {
|
|
11030
11779
|
var _a, _b;
|
|
11031
11780
|
if (!this.isPlaying || this.currentState === AvatarState.paused) {
|
|
@@ -11038,6 +11787,11 @@ class AvatarController {
|
|
|
11038
11787
|
(_b = this.onConversationState) == null ? void 0 : _b.call(this, this.mapToConversationState(AvatarState.paused));
|
|
11039
11788
|
logger.log("[AvatarController] Playback paused");
|
|
11040
11789
|
}
|
|
11790
|
+
/**
|
|
11791
|
+
* Resume playback (from paused state)
|
|
11792
|
+
* Resume audio playback and restart render loop
|
|
11793
|
+
* Animation will continue from paused frame (because animation time base comes from audio, will auto-sync)
|
|
11794
|
+
*/
|
|
11041
11795
|
async resume() {
|
|
11042
11796
|
var _a, _b;
|
|
11043
11797
|
if (!this.isPlaying || this.currentState !== AvatarState.paused) {
|
|
@@ -11050,6 +11804,9 @@ class AvatarController {
|
|
|
11050
11804
|
(_b = this.onConversationState) == null ? void 0 : _b.call(this, this.mapToConversationState(AvatarState.playing));
|
|
11051
11805
|
logger.log("[AvatarController] Playback resumed");
|
|
11052
11806
|
}
|
|
11807
|
+
/**
|
|
11808
|
+
* Interrupt current playback
|
|
11809
|
+
*/
|
|
11053
11810
|
interrupt() {
|
|
11054
11811
|
var _a;
|
|
11055
11812
|
if (this.currentState === AvatarState.paused) {
|
|
@@ -11062,6 +11819,9 @@ class AvatarController {
|
|
|
11062
11819
|
this.resetConversationIdState();
|
|
11063
11820
|
this.isAudioOnlyMode = false;
|
|
11064
11821
|
}
|
|
11822
|
+
/**
|
|
11823
|
+
* Clear all data and resources
|
|
11824
|
+
*/
|
|
11065
11825
|
clear() {
|
|
11066
11826
|
var _a, _b;
|
|
11067
11827
|
if (this.isPlaying) {
|
|
@@ -11079,6 +11839,11 @@ class AvatarController {
|
|
|
11079
11839
|
}
|
|
11080
11840
|
this.reqEnd = false;
|
|
11081
11841
|
}
|
|
11842
|
+
/**
|
|
11843
|
+
* Dispose controller, clean up all callbacks to avoid memory leaks
|
|
11844
|
+
* Should be called when AvatarView.dispose()
|
|
11845
|
+
* @internal
|
|
11846
|
+
*/
|
|
11082
11847
|
dispose() {
|
|
11083
11848
|
this.onConnectionState = null;
|
|
11084
11849
|
this.onConversationState = null;
|
|
@@ -11086,6 +11851,11 @@ class AvatarController {
|
|
|
11086
11851
|
this.renderCallback = void 0;
|
|
11087
11852
|
this.eventListeners.clear();
|
|
11088
11853
|
}
|
|
11854
|
+
// ========== Internal Helper Methods ==========
|
|
11855
|
+
/**
|
|
11856
|
+
* Generate new conversation ID and log conversation started event
|
|
11857
|
+
* @private
|
|
11858
|
+
*/
|
|
11089
11859
|
generateAndLogNewConversationId() {
|
|
11090
11860
|
const conversationId = idManager.generateNewConversationId();
|
|
11091
11861
|
logEvent("character_manager", "info", {
|
|
@@ -11095,6 +11865,10 @@ class AvatarController {
|
|
|
11095
11865
|
});
|
|
11096
11866
|
return conversationId;
|
|
11097
11867
|
}
|
|
11868
|
+
/**
|
|
11869
|
+
* Clear playback data (keyframes, audio chunks, and playback state)
|
|
11870
|
+
* @private
|
|
11871
|
+
*/
|
|
11098
11872
|
clearPlaybackData() {
|
|
11099
11873
|
this.currentKeyframes = [];
|
|
11100
11874
|
this.pendingAudioChunks = [];
|
|
@@ -11115,6 +11889,10 @@ class AvatarController {
|
|
|
11115
11889
|
};
|
|
11116
11890
|
}
|
|
11117
11891
|
}
|
|
11892
|
+
/**
|
|
11893
|
+
* Reset conversation ID state (for both network and external modes)
|
|
11894
|
+
* @private
|
|
11895
|
+
*/
|
|
11118
11896
|
resetConversationIdState() {
|
|
11119
11897
|
if (this.networkLayer) {
|
|
11120
11898
|
this.networkLayer.resetConversationId();
|
|
@@ -11135,6 +11913,10 @@ class AvatarController {
|
|
|
11135
11913
|
};
|
|
11136
11914
|
}
|
|
11137
11915
|
}
|
|
11916
|
+
/**
|
|
11917
|
+
* Get effective conversation ID (handles both SDK and host modes)
|
|
11918
|
+
* @private
|
|
11919
|
+
*/
|
|
11138
11920
|
getEffectiveConversationId() {
|
|
11139
11921
|
var _a;
|
|
11140
11922
|
if (this.playbackMode === DrivingServiceMode.sdk) {
|
|
@@ -11143,18 +11925,35 @@ class AvatarController {
|
|
|
11143
11925
|
return this.currentConversationId;
|
|
11144
11926
|
}
|
|
11145
11927
|
}
|
|
11928
|
+
// ========== Internal Methods (for NetworkLayer and AvatarView use) ==========
|
|
11929
|
+
/**
|
|
11930
|
+
* Start streaming playback (internal method, called by NetworkLayer or playback())
|
|
11931
|
+
* @internal
|
|
11932
|
+
*/
|
|
11146
11933
|
startStreamingPlayback() {
|
|
11147
11934
|
return this.startStreamingPlaybackInternal();
|
|
11148
11935
|
}
|
|
11936
|
+
/**
|
|
11937
|
+
* Set render callback (called by AvatarView)
|
|
11938
|
+
* @internal
|
|
11939
|
+
*/
|
|
11149
11940
|
setRenderCallback(callback, characterHandle) {
|
|
11150
11941
|
this.renderCallback = callback;
|
|
11151
11942
|
if (characterHandle !== void 0) {
|
|
11152
11943
|
this.characterHandle = characterHandle;
|
|
11153
11944
|
}
|
|
11154
11945
|
}
|
|
11946
|
+
/**
|
|
11947
|
+
* Set character ID (for multi-character support, used for eye tracking)
|
|
11948
|
+
* @internal
|
|
11949
|
+
*/
|
|
11155
11950
|
setCharacterId(characterId) {
|
|
11156
11951
|
this.characterId = characterId;
|
|
11157
11952
|
}
|
|
11953
|
+
/**
|
|
11954
|
+
* Get point cloud count of the current avatar
|
|
11955
|
+
* @returns Point cloud count, or null if avatar is not loaded
|
|
11956
|
+
*/
|
|
11158
11957
|
getPointCount() {
|
|
11159
11958
|
const avatarCore = AvatarSDK.getAvatarCore();
|
|
11160
11959
|
if (!avatarCore || !this.characterHandle) {
|
|
@@ -11162,6 +11961,11 @@ class AvatarController {
|
|
|
11162
11961
|
}
|
|
11163
11962
|
return avatarCore.getPointCount(this.characterHandle);
|
|
11164
11963
|
}
|
|
11964
|
+
/**
|
|
11965
|
+
* Set post-processing configuration
|
|
11966
|
+
* These parameters will be applied in real-time to animation parameters returned by the server
|
|
11967
|
+
* @param config Post-processing configuration, or null to clear
|
|
11968
|
+
*/
|
|
11165
11969
|
setPostProcessingConfig(config) {
|
|
11166
11970
|
this.postProcessingConfig = config;
|
|
11167
11971
|
if (config == null ? void 0 : config.eyefocus) {
|
|
@@ -11173,6 +11977,10 @@ class AvatarController {
|
|
|
11173
11977
|
this.rerenderCurrentFrame();
|
|
11174
11978
|
}
|
|
11175
11979
|
}
|
|
11980
|
+
/**
|
|
11981
|
+
* 设置完整的 eye tracking 配置到 C++
|
|
11982
|
+
* @private
|
|
11983
|
+
*/
|
|
11176
11984
|
async setEyeTrackingConfig(eyefocus) {
|
|
11177
11985
|
const avatarCore = AvatarSDK.getAvatarCore();
|
|
11178
11986
|
if (!avatarCore || !this.characterId) {
|
|
@@ -11196,6 +12004,10 @@ class AvatarController {
|
|
|
11196
12004
|
logger.warn("[AvatarController] Failed to set eye tracking config:", error instanceof Error ? error.message : String(error));
|
|
11197
12005
|
}
|
|
11198
12006
|
}
|
|
12007
|
+
/**
|
|
12008
|
+
* 重新渲染当前帧(用于暂停状态下更新后处理参数或相机配置)
|
|
12009
|
+
* @internal
|
|
12010
|
+
*/
|
|
11199
12011
|
async rerenderCurrentFrameIfPaused() {
|
|
11200
12012
|
if (this.currentState !== AvatarState.paused || !this.renderCallback) {
|
|
11201
12013
|
return;
|
|
@@ -11228,9 +12040,17 @@ class AvatarController {
|
|
|
11228
12040
|
logger.error("[AvatarController] Failed to rerender current frame:", error instanceof Error ? error.message : String(error));
|
|
11229
12041
|
}
|
|
11230
12042
|
}
|
|
12043
|
+
/**
|
|
12044
|
+
* 重新渲染当前帧(用于暂停状态下更新后处理参数)
|
|
12045
|
+
* @private
|
|
12046
|
+
*/
|
|
11231
12047
|
async rerenderCurrentFrame() {
|
|
11232
12048
|
await this.rerenderCurrentFrameIfPaused();
|
|
11233
12049
|
}
|
|
12050
|
+
/**
|
|
12051
|
+
* Transition complete notification (called by AvatarView)
|
|
12052
|
+
* @internal
|
|
12053
|
+
*/
|
|
11234
12054
|
onTransitionComplete() {
|
|
11235
12055
|
var _a;
|
|
11236
12056
|
const streamingPlayer = (_a = this.animationPlayer) == null ? void 0 : _a.getStreamingPlayer();
|
|
@@ -11238,6 +12058,11 @@ class AvatarController {
|
|
|
11238
12058
|
streamingPlayer.play();
|
|
11239
12059
|
}
|
|
11240
12060
|
}
|
|
12061
|
+
/**
|
|
12062
|
+
* Set audio playback volume
|
|
12063
|
+
* Note: This only controls the avatar audio player volume, not the system volume
|
|
12064
|
+
* @param volume Volume value, range from 0.0 to 1.0 (0.0 = mute, 1.0 = max volume)
|
|
12065
|
+
*/
|
|
11241
12066
|
setVolume(volume) {
|
|
11242
12067
|
var _a;
|
|
11243
12068
|
if (volume < 0 || volume > 1) {
|
|
@@ -11251,10 +12076,18 @@ class AvatarController {
|
|
|
11251
12076
|
volume
|
|
11252
12077
|
});
|
|
11253
12078
|
}
|
|
12079
|
+
/**
|
|
12080
|
+
* Get current audio playback volume
|
|
12081
|
+
* @returns Current volume value (0.0 - 1.0)
|
|
12082
|
+
*/
|
|
11254
12083
|
getVolume() {
|
|
11255
12084
|
var _a;
|
|
11256
12085
|
return ((_a = this.animationPlayer) == null ? void 0 : _a.getVolume()) ?? 1;
|
|
11257
12086
|
}
|
|
12087
|
+
/**
|
|
12088
|
+
* Provide interface for AvatarView to register internal events
|
|
12089
|
+
* @internal
|
|
12090
|
+
*/
|
|
11258
12091
|
setupInternalEventListeners(callbacks) {
|
|
11259
12092
|
if (callbacks.onKeyframesUpdate) {
|
|
11260
12093
|
this.registerEventListener("keyframesUpdate", callbacks.onKeyframesUpdate);
|
|
@@ -11269,6 +12102,10 @@ class AvatarController {
|
|
|
11269
12102
|
this.registerEventListener("interrupt", callbacks.onInterrupt);
|
|
11270
12103
|
}
|
|
11271
12104
|
}
|
|
12105
|
+
// ========== Private Methods ==========
|
|
12106
|
+
/**
|
|
12107
|
+
* Start streaming playback (internal implementation)
|
|
12108
|
+
*/
|
|
11272
12109
|
async startStreamingPlaybackInternal() {
|
|
11273
12110
|
var _a, _b, _c;
|
|
11274
12111
|
this.checkAudioContextInitialized();
|
|
@@ -11336,6 +12173,18 @@ class AvatarController {
|
|
|
11336
12173
|
this.isStartingPlayback = false;
|
|
11337
12174
|
}
|
|
11338
12175
|
}
|
|
12176
|
+
/**
|
|
12177
|
+
* Playback loop: Calculate animation frame based on audio time, notify render layer to render
|
|
12178
|
+
*/
|
|
12179
|
+
/**
|
|
12180
|
+
* 检测播放是否卡住(在过渡动画完成后)
|
|
12181
|
+
* 注意:AudioContext suspended 的情况现在由 StreamingAudioPlayer 自动处理,不再作为触发条件
|
|
12182
|
+
* 此函数主要用于检测其他原因导致的播放卡住(如网络问题、音频数据问题等)
|
|
12183
|
+
*
|
|
12184
|
+
* @param audioTime 当前音频时间
|
|
12185
|
+
* @returns true 如果检测到卡住并已上报,false 否则
|
|
12186
|
+
* @internal
|
|
12187
|
+
*/
|
|
11339
12188
|
checkPlaybackStuck(audioTime) {
|
|
11340
12189
|
var _a, _b, _c, _d, _e2, _f;
|
|
11341
12190
|
const streamingPlayer = (_a = this.animationPlayer) == null ? void 0 : _a.getStreamingPlayer();
|
|
@@ -11378,6 +12227,7 @@ class AvatarController {
|
|
|
11378
12227
|
event: "playback_stuck_after_transition",
|
|
11379
12228
|
avatar_id: this.avatar.id,
|
|
11380
12229
|
conversationId: ((_e2 = this.networkLayer) == null ? void 0 : _e2.getCurrentConversationId()) || void 0,
|
|
12230
|
+
// 诊断信息(包含 audioContextState 用于诊断,但不作为触发条件)
|
|
11381
12231
|
audioContextState,
|
|
11382
12232
|
audioTime,
|
|
11383
12233
|
audioTimeZero: audioTime === 0,
|
|
@@ -11488,12 +12338,22 @@ class AvatarController {
|
|
|
11488
12338
|
};
|
|
11489
12339
|
this.playbackLoopId = requestAnimationFrame(playLoop);
|
|
11490
12340
|
}
|
|
12341
|
+
/**
|
|
12342
|
+
* Stop playback loop
|
|
12343
|
+
*/
|
|
11491
12344
|
stopPlaybackLoop() {
|
|
11492
12345
|
if (this.playbackLoopId) {
|
|
11493
12346
|
cancelAnimationFrame(this.playbackLoopId);
|
|
11494
12347
|
this.playbackLoopId = null;
|
|
11495
12348
|
}
|
|
11496
12349
|
}
|
|
12350
|
+
// ========== Audio Only Mode ==========
|
|
12351
|
+
/**
|
|
12352
|
+
* 启用音频独立播放模式(当服务器错误或超时时调用)
|
|
12353
|
+
* 此模式下,音频会独立播放,不依赖动画数据
|
|
12354
|
+
* 一旦启用,本次会话后续的动画数据将被忽略
|
|
12355
|
+
* @private
|
|
12356
|
+
*/
|
|
11497
12357
|
enableAudioOnlyMode() {
|
|
11498
12358
|
if (this.isAudioOnlyMode) {
|
|
11499
12359
|
return;
|
|
@@ -11507,6 +12367,10 @@ class AvatarController {
|
|
|
11507
12367
|
});
|
|
11508
12368
|
}
|
|
11509
12369
|
}
|
|
12370
|
+
/**
|
|
12371
|
+
* 音频独立播放(完全独立的逻辑,不影响正常播放流程)
|
|
12372
|
+
* @private
|
|
12373
|
+
*/
|
|
11510
12374
|
async startAudioOnlyPlayback() {
|
|
11511
12375
|
var _a, _b;
|
|
11512
12376
|
if (!this.animationPlayer) {
|
|
@@ -11553,6 +12417,11 @@ class AvatarController {
|
|
|
11553
12417
|
throw error;
|
|
11554
12418
|
}
|
|
11555
12419
|
}
|
|
12420
|
+
/**
|
|
12421
|
+
* 音频监控循环(仅用于音频独立模式)
|
|
12422
|
+
* 只检测音频是否结束,不进行动画渲染
|
|
12423
|
+
* @private
|
|
12424
|
+
*/
|
|
11556
12425
|
startAudioMonitoringLoop() {
|
|
11557
12426
|
if (this.playbackLoopId) {
|
|
11558
12427
|
return;
|
|
@@ -11571,6 +12440,9 @@ class AvatarController {
|
|
|
11571
12440
|
};
|
|
11572
12441
|
this.playbackLoopId = requestAnimationFrame(monitorLoop);
|
|
11573
12442
|
}
|
|
12443
|
+
/**
|
|
12444
|
+
* Stop playback
|
|
12445
|
+
*/
|
|
11574
12446
|
stopPlayback() {
|
|
11575
12447
|
var _a;
|
|
11576
12448
|
this.stopPlaybackLoop();
|
|
@@ -11585,12 +12457,19 @@ class AvatarController {
|
|
|
11585
12457
|
this.currentState = AvatarState.idle;
|
|
11586
12458
|
(_a = this.onConversationState) == null ? void 0 : _a.call(this, this.mapToConversationState(AvatarState.idle));
|
|
11587
12459
|
}
|
|
12460
|
+
/**
|
|
12461
|
+
* Clean up players
|
|
12462
|
+
*/
|
|
11588
12463
|
cleanupPlayers() {
|
|
11589
12464
|
if (this.animationPlayer) {
|
|
11590
12465
|
this.animationPlayer.dispose();
|
|
11591
12466
|
this.animationPlayer = null;
|
|
11592
12467
|
}
|
|
11593
12468
|
}
|
|
12469
|
+
/**
|
|
12470
|
+
* Add audio chunk to buffer
|
|
12471
|
+
* Note: animationPlayer should already be initialized before calling this method
|
|
12472
|
+
*/
|
|
11594
12473
|
addAudioChunkToBuffer(data, isLast) {
|
|
11595
12474
|
if (!this.animationPlayer) {
|
|
11596
12475
|
logger.warn("[AvatarController] animationPlayer is null in addAudioChunkToBuffer, this should not happen");
|
|
@@ -11603,18 +12482,29 @@ class AvatarController {
|
|
|
11603
12482
|
this.pendingAudioChunks.push({ data, isLast });
|
|
11604
12483
|
}
|
|
11605
12484
|
}
|
|
12485
|
+
/**
|
|
12486
|
+
* Event system
|
|
12487
|
+
*/
|
|
11606
12488
|
registerEventListener(event, callback) {
|
|
11607
12489
|
if (!this.eventListeners.has(event)) {
|
|
11608
12490
|
this.eventListeners.set(event, /* @__PURE__ */ new Set());
|
|
11609
12491
|
}
|
|
11610
12492
|
this.eventListeners.get(event).add(callback);
|
|
11611
12493
|
}
|
|
12494
|
+
/**
|
|
12495
|
+
* Emit event
|
|
12496
|
+
*/
|
|
11612
12497
|
emit(event, data) {
|
|
11613
12498
|
const listeners = this.eventListeners.get(event);
|
|
11614
12499
|
if (listeners) {
|
|
11615
12500
|
listeners.forEach((callback) => callback(data));
|
|
11616
12501
|
}
|
|
11617
12502
|
}
|
|
12503
|
+
/**
|
|
12504
|
+
* Apply post-processing parameters to a Flame (proto format)
|
|
12505
|
+
* Used for transition animations
|
|
12506
|
+
* @internal
|
|
12507
|
+
*/
|
|
11618
12508
|
applyPostProcessingToFlame(flame) {
|
|
11619
12509
|
if (!this.postProcessingConfig) {
|
|
11620
12510
|
return flame;
|
|
@@ -11623,6 +12513,10 @@ class AvatarController {
|
|
|
11623
12513
|
wasmParams = this.applyPostProcessingToParams(wasmParams);
|
|
11624
12514
|
return convertWasmParamsToProtoFlame(wasmParams);
|
|
11625
12515
|
}
|
|
12516
|
+
/**
|
|
12517
|
+
* Apply post-processing parameters to animation parameters
|
|
12518
|
+
* @private
|
|
12519
|
+
*/
|
|
11626
12520
|
applyPostProcessingToParams(baseParams) {
|
|
11627
12521
|
if (!this.postProcessingConfig) {
|
|
11628
12522
|
return baseParams;
|
|
@@ -11696,6 +12590,11 @@ class AvatarController {
|
|
|
11696
12590
|
}
|
|
11697
12591
|
return result2;
|
|
11698
12592
|
}
|
|
12593
|
+
/**
|
|
12594
|
+
* 上报 driving_service_latency 事件(Host 模式)
|
|
12595
|
+
* 每轮会话只上报一次(在收到首帧时)
|
|
12596
|
+
* @private
|
|
12597
|
+
*/
|
|
11699
12598
|
reportDrivingServiceLatency(conversationId) {
|
|
11700
12599
|
if (!conversationId || this.playbackMode !== DrivingServiceMode.host) {
|
|
11701
12600
|
return;
|
|
@@ -11704,10 +12603,15 @@ class AvatarController {
|
|
|
11704
12603
|
logEvent("driving_service_latency", "info", {
|
|
11705
12604
|
req_id: conversationId,
|
|
11706
12605
|
dsm: "host",
|
|
12606
|
+
// Host 模式
|
|
11707
12607
|
tap_0: metrics.startTimestamp || 0,
|
|
12608
|
+
// 必须有非零值
|
|
11708
12609
|
tap_1: metrics.tap1Timestamp || 0,
|
|
12610
|
+
// 缺省值为 0
|
|
11709
12611
|
tap_2: metrics.tap2Timestamp || 0,
|
|
12612
|
+
// 缺省值为 0
|
|
11710
12613
|
tap_f: metrics.recvFirstFlameTimestamp || 0
|
|
12614
|
+
// 必须有非零值
|
|
11711
12615
|
});
|
|
11712
12616
|
}
|
|
11713
12617
|
}
|
|
@@ -11731,12 +12635,21 @@ function errorToMessage(err) {
|
|
|
11731
12635
|
return String(err);
|
|
11732
12636
|
}
|
|
11733
12637
|
const _PwaCacheManager = class _PwaCacheManager {
|
|
12638
|
+
/**
|
|
12639
|
+
* 检查是否支持 Cache API
|
|
12640
|
+
*/
|
|
11734
12641
|
static isSupported() {
|
|
11735
12642
|
return typeof caches !== "undefined";
|
|
11736
12643
|
}
|
|
12644
|
+
/**
|
|
12645
|
+
* 获取角色缓存名称
|
|
12646
|
+
*/
|
|
11737
12647
|
static getCharacterCacheName(characterId) {
|
|
11738
12648
|
return `${_PwaCacheManager.CHARACTER_CACHE_PREFIX}${characterId}${_PwaCacheManager.CHARACTER_CACHE_SUFFIX}`;
|
|
11739
12649
|
}
|
|
12650
|
+
/**
|
|
12651
|
+
* 从角色缓存获取资源
|
|
12652
|
+
*/
|
|
11740
12653
|
static async getCharacterResource(characterId, url) {
|
|
11741
12654
|
if (!_PwaCacheManager.isSupported()) {
|
|
11742
12655
|
return null;
|
|
@@ -11756,6 +12669,9 @@ const _PwaCacheManager = class _PwaCacheManager {
|
|
|
11756
12669
|
return null;
|
|
11757
12670
|
}
|
|
11758
12671
|
}
|
|
12672
|
+
/**
|
|
12673
|
+
* 将角色资源写入缓存
|
|
12674
|
+
*/
|
|
11759
12675
|
static async putCharacterResource(characterId, url, data) {
|
|
11760
12676
|
if (!_PwaCacheManager.isSupported()) {
|
|
11761
12677
|
return;
|
|
@@ -11775,6 +12691,9 @@ const _PwaCacheManager = class _PwaCacheManager {
|
|
|
11775
12691
|
logger.warn(`[PwaCacheManager] Failed to put character resource to cache:`, error);
|
|
11776
12692
|
}
|
|
11777
12693
|
}
|
|
12694
|
+
/**
|
|
12695
|
+
* 从模板缓存获取资源
|
|
12696
|
+
*/
|
|
11778
12697
|
static async getTemplateResource(url) {
|
|
11779
12698
|
if (!_PwaCacheManager.isSupported()) {
|
|
11780
12699
|
return null;
|
|
@@ -11793,6 +12712,10 @@ const _PwaCacheManager = class _PwaCacheManager {
|
|
|
11793
12712
|
return null;
|
|
11794
12713
|
}
|
|
11795
12714
|
}
|
|
12715
|
+
/**
|
|
12716
|
+
* 将模板资源写入缓存
|
|
12717
|
+
* 模板资源不设置数量限制,永久保留直到版本更新
|
|
12718
|
+
*/
|
|
11796
12719
|
static async putTemplateResource(url, data) {
|
|
11797
12720
|
if (!_PwaCacheManager.isSupported()) {
|
|
11798
12721
|
return;
|
|
@@ -11805,6 +12728,9 @@ const _PwaCacheManager = class _PwaCacheManager {
|
|
|
11805
12728
|
logger.warn(`[PwaCacheManager] Failed to put template resource to cache:`, error);
|
|
11806
12729
|
}
|
|
11807
12730
|
}
|
|
12731
|
+
/**
|
|
12732
|
+
* 清理角色缓存
|
|
12733
|
+
*/
|
|
11808
12734
|
static async clearCharacterCache(characterId) {
|
|
11809
12735
|
if (!_PwaCacheManager.isSupported()) {
|
|
11810
12736
|
return;
|
|
@@ -11817,6 +12743,11 @@ const _PwaCacheManager = class _PwaCacheManager {
|
|
|
11817
12743
|
logger.warn(`[PwaCacheManager] Failed to clear character cache:`, error);
|
|
11818
12744
|
}
|
|
11819
12745
|
}
|
|
12746
|
+
/**
|
|
12747
|
+
* 检查模板缓存版本,如果版本变化则清理
|
|
12748
|
+
* 使用独立的模板资源版本号(不依赖 SDK 版本),这样不同 SDK 版本可以共享相同模板资源的缓存
|
|
12749
|
+
* @returns true 如果版本变化并清理了缓存,false 否则
|
|
12750
|
+
*/
|
|
11820
12751
|
static async checkTemplateCacheVersion() {
|
|
11821
12752
|
if (!_PwaCacheManager.isSupported()) {
|
|
11822
12753
|
return false;
|
|
@@ -11841,14 +12772,17 @@ const _PwaCacheManager = class _PwaCacheManager {
|
|
|
11841
12772
|
}
|
|
11842
12773
|
}
|
|
11843
12774
|
};
|
|
12775
|
+
// 模板缓存版本(独立于 SDK 版本,当模板资源更新时需要手动更新此版本号)
|
|
11844
12776
|
__publicField(_PwaCacheManager, "TEMPLATE_RESOURCE_VERSION", "1.0.0");
|
|
11845
12777
|
__publicField(_PwaCacheManager, "TEMPLATE_CACHE_NAME", `spatialwalk-sdk-template-cache-${_PwaCacheManager.TEMPLATE_RESOURCE_VERSION}`);
|
|
11846
12778
|
__publicField(_PwaCacheManager, "TEMPLATE_VERSION_STORAGE_KEY", "spatialwalk-sdk-template-cache-version");
|
|
12779
|
+
// 角色缓存前缀和后缀
|
|
11847
12780
|
__publicField(_PwaCacheManager, "CHARACTER_CACHE_PREFIX", "spatialwalk-sdk-character-");
|
|
11848
12781
|
__publicField(_PwaCacheManager, "CHARACTER_CACHE_SUFFIX", "-cache");
|
|
12782
|
+
// 角色缓存 LRU 限制:最多 1000 个资源条目(支持约 250 个角色,每个角色 4 个资源)
|
|
11849
12783
|
__publicField(_PwaCacheManager, "MAX_CHARACTER_CACHE_ENTRIES", 1e3);
|
|
11850
12784
|
let PwaCacheManager = _PwaCacheManager;
|
|
11851
|
-
const pwaCacheManager = Object.freeze(Object.defineProperty({
|
|
12785
|
+
const pwaCacheManager = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
11852
12786
|
__proto__: null,
|
|
11853
12787
|
PwaCacheManager
|
|
11854
12788
|
}, Symbol.toStringTag, { value: "Module" }));
|
|
@@ -11943,6 +12877,11 @@ class AvatarDownloader {
|
|
|
11943
12877
|
__publicField(this, "baseAssetsPath");
|
|
11944
12878
|
this.baseAssetsPath = baseAssetsPath;
|
|
11945
12879
|
}
|
|
12880
|
+
/**
|
|
12881
|
+
* Load template resources from CharacterMeta flame CDN URLs
|
|
12882
|
+
* Falls back to global CDN config if not provided by API
|
|
12883
|
+
* @internal
|
|
12884
|
+
*/
|
|
11946
12885
|
async loadTemplateResources(flameResources, progressCallback = null) {
|
|
11947
12886
|
var _a, _b, _c, _d;
|
|
11948
12887
|
await PwaCacheManager.checkTemplateCacheVersion();
|
|
@@ -12000,6 +12939,11 @@ class AvatarDownloader {
|
|
|
12000
12939
|
await Promise.all(promises);
|
|
12001
12940
|
return templateResources;
|
|
12002
12941
|
}
|
|
12942
|
+
/**
|
|
12943
|
+
* Load global FLAME template resources from CDN
|
|
12944
|
+
* Uses centralized FLAME CDN config (shared across all characters)
|
|
12945
|
+
* @internal 供内部使用(测试、调试等场景),SDK 初始化默认使用本地打包的资源
|
|
12946
|
+
*/
|
|
12003
12947
|
async loadGlobalFlameResources(progressCallback = null) {
|
|
12004
12948
|
var _a;
|
|
12005
12949
|
await PwaCacheManager.checkTemplateCacheVersion();
|
|
@@ -12065,6 +13009,10 @@ class AvatarDownloader {
|
|
|
12065
13009
|
throw error;
|
|
12066
13010
|
}
|
|
12067
13011
|
}
|
|
13012
|
+
/**
|
|
13013
|
+
* Load camera settings from CharacterMeta (optional)
|
|
13014
|
+
* @internal
|
|
13015
|
+
*/
|
|
12068
13016
|
async loadCameraSettings(characterMeta) {
|
|
12069
13017
|
var _a, _b;
|
|
12070
13018
|
const cameraUrl = (_b = (_a = characterMeta.camera) == null ? void 0 : _a.resource) == null ? void 0 : _b.remote;
|
|
@@ -12087,6 +13035,10 @@ class AvatarDownloader {
|
|
|
12087
13035
|
return void 0;
|
|
12088
13036
|
}
|
|
12089
13037
|
}
|
|
13038
|
+
/**
|
|
13039
|
+
* Load character data from CharacterMeta (iOS compatible)
|
|
13040
|
+
* @internal
|
|
13041
|
+
*/
|
|
12090
13042
|
async loadCharacterData(characterMeta, options) {
|
|
12091
13043
|
var _a, _b, _c, _d, _e2, _f, _g, _h, _i2, _j;
|
|
12092
13044
|
const { progressCallback = null } = options || {};
|
|
@@ -12189,6 +13141,10 @@ class AvatarDownloader {
|
|
|
12189
13141
|
});
|
|
12190
13142
|
return characterData;
|
|
12191
13143
|
}
|
|
13144
|
+
/**
|
|
13145
|
+
* Preload all resources (template + character data + camera info + settings)
|
|
13146
|
+
* @internal
|
|
13147
|
+
*/
|
|
12192
13148
|
async preloadResources(characterMeta, options) {
|
|
12193
13149
|
const { progressCallback = null } = options || {};
|
|
12194
13150
|
const [characterData, preloadCameraSettings] = await Promise.all([
|
|
@@ -12210,6 +13166,13 @@ class AvatarDownloader {
|
|
|
12210
13166
|
characterSettings: characterMeta.characterSettings
|
|
12211
13167
|
};
|
|
12212
13168
|
}
|
|
13169
|
+
// ============ API Client Methods ============
|
|
13170
|
+
/**
|
|
13171
|
+
* Get AvatarKit SDK API Client (api.open.spatialwalk.top for cn, api.intl.spatialwalk.cloud for intl)
|
|
13172
|
+
* Used for: character details and resource URLs (public endpoints, no auth required)
|
|
13173
|
+
* Note: This endpoint does not require authentication, so we don't add X-App-Id or Authorization headers
|
|
13174
|
+
* to avoid CORS preflight requests for simple GET requests
|
|
13175
|
+
*/
|
|
12213
13176
|
getSdkApiClient() {
|
|
12214
13177
|
return {
|
|
12215
13178
|
async request(url, options = {}) {
|
|
@@ -12255,6 +13218,13 @@ class AvatarDownloader {
|
|
|
12255
13218
|
}
|
|
12256
13219
|
};
|
|
12257
13220
|
}
|
|
13221
|
+
/**
|
|
13222
|
+
* Get single character by ID from AvatarKit SDK API (v2, iOS compatible)
|
|
13223
|
+
* Domain: api.open.spatialwalk.top (cn) / api.intl.spatialwalk.cloud (intl)
|
|
13224
|
+
* Auth: Public endpoint, no authentication required
|
|
13225
|
+
* Returns CharacterMeta with nested resource structure
|
|
13226
|
+
* @internal
|
|
13227
|
+
*/
|
|
12258
13228
|
async getCharacterById(characterId) {
|
|
12259
13229
|
var _a;
|
|
12260
13230
|
const startTime = Date.now();
|
|
@@ -12288,7 +13258,7 @@ class AvatarDownloader {
|
|
|
12288
13258
|
}
|
|
12289
13259
|
}
|
|
12290
13260
|
}
|
|
12291
|
-
const AvatarDownloader$1 = Object.freeze(Object.defineProperty({
|
|
13261
|
+
const AvatarDownloader$1 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
12292
13262
|
__proto__: null,
|
|
12293
13263
|
AvatarDownloader
|
|
12294
13264
|
}, Symbol.toStringTag, { value: "Module" }));
|
|
@@ -12297,15 +13267,25 @@ const _AvatarManager = class _AvatarManager {
|
|
|
12297
13267
|
__publicField(this, "avatarDownloader", null);
|
|
12298
13268
|
__publicField(this, "avatarCache", /* @__PURE__ */ new Map());
|
|
12299
13269
|
__publicField(this, "loadingPromises", /* @__PURE__ */ new Map());
|
|
13270
|
+
// 下载队列:确保资源下载串行执行
|
|
12300
13271
|
__publicField(this, "downloadQueue", []);
|
|
12301
13272
|
__publicField(this, "isDownloading", false);
|
|
12302
13273
|
}
|
|
13274
|
+
/**
|
|
13275
|
+
* Access via global singleton
|
|
13276
|
+
*/
|
|
12303
13277
|
static get shared() {
|
|
12304
13278
|
if (!this._instance) {
|
|
12305
13279
|
this._instance = new _AvatarManager();
|
|
12306
13280
|
}
|
|
12307
13281
|
return this._instance;
|
|
12308
13282
|
}
|
|
13283
|
+
/**
|
|
13284
|
+
* Load avatar
|
|
13285
|
+
* @param id Avatar ID
|
|
13286
|
+
* @param onProgress Progress callback
|
|
13287
|
+
* @returns Promise<Avatar>
|
|
13288
|
+
*/
|
|
12309
13289
|
async load(id, onProgress) {
|
|
12310
13290
|
const loadingPromise = this.loadingPromises.get(id);
|
|
12311
13291
|
if (loadingPromise) {
|
|
@@ -12354,6 +13334,9 @@ const _AvatarManager = class _AvatarManager {
|
|
|
12354
13334
|
this.processDownloadQueue();
|
|
12355
13335
|
return loadPromise;
|
|
12356
13336
|
}
|
|
13337
|
+
/**
|
|
13338
|
+
* 处理下载队列(确保串行执行)
|
|
13339
|
+
*/
|
|
12357
13340
|
async processDownloadQueue() {
|
|
12358
13341
|
if (this.isDownloading || this.downloadQueue.length === 0) {
|
|
12359
13342
|
return;
|
|
@@ -12371,6 +13354,9 @@ const _AvatarManager = class _AvatarManager {
|
|
|
12371
13354
|
this.processDownloadQueue();
|
|
12372
13355
|
}
|
|
12373
13356
|
}
|
|
13357
|
+
/**
|
|
13358
|
+
* 执行实际的加载逻辑(私有方法)
|
|
13359
|
+
*/
|
|
12374
13360
|
async doLoad(id, characterMeta, onProgress) {
|
|
12375
13361
|
try {
|
|
12376
13362
|
logger.log("[AvatarManager] Step 1: Downloading resources...");
|
|
@@ -12401,22 +13387,31 @@ const _AvatarManager = class _AvatarManager {
|
|
|
12401
13387
|
throw error;
|
|
12402
13388
|
}
|
|
12403
13389
|
}
|
|
13390
|
+
/**
|
|
13391
|
+
* Get cached avatar
|
|
13392
|
+
* @param id Avatar ID
|
|
13393
|
+
* @returns Avatar instance, or undefined if not in cache
|
|
13394
|
+
*/
|
|
12404
13395
|
retrieve(id) {
|
|
12405
13396
|
return this.avatarCache.get(id);
|
|
12406
13397
|
}
|
|
13398
|
+
/**
|
|
13399
|
+
* Clear cached avatar for specified ID
|
|
13400
|
+
* @param id Avatar ID
|
|
13401
|
+
*/
|
|
12407
13402
|
clear(id) {
|
|
12408
13403
|
const removed = this.avatarCache.delete(id);
|
|
12409
13404
|
if (removed) {
|
|
12410
13405
|
logger.log(`[AvatarManager] Cleared avatar cache for id: ${id}`);
|
|
12411
13406
|
}
|
|
12412
13407
|
}
|
|
13408
|
+
/**
|
|
13409
|
+
* Clear all avatar cache and resource loader cache
|
|
13410
|
+
*/
|
|
12413
13411
|
clearAll() {
|
|
12414
13412
|
this.avatarCache.clear();
|
|
12415
13413
|
logger.log("[AvatarManager] Cleared all avatar cache");
|
|
12416
13414
|
}
|
|
12417
|
-
clearCache() {
|
|
12418
|
-
this.clearAll();
|
|
12419
|
-
}
|
|
12420
13415
|
};
|
|
12421
13416
|
__publicField(_AvatarManager, "_instance", null);
|
|
12422
13417
|
let AvatarManager = _AvatarManager;
|
|
@@ -12523,11 +13518,14 @@ class WebGLRenderer {
|
|
|
12523
13518
|
__publicField(this, "splatCount");
|
|
12524
13519
|
__publicField(this, "isInitialized");
|
|
12525
13520
|
__publicField(this, "splatBufferSize");
|
|
13521
|
+
// 跟踪当前 buffer 大小
|
|
13522
|
+
// Render texture framebuffer
|
|
12526
13523
|
__publicField(this, "framebuffer", null);
|
|
12527
13524
|
__publicField(this, "renderTexture", null);
|
|
12528
13525
|
__publicField(this, "depthBuffer", null);
|
|
12529
13526
|
__publicField(this, "framebufferWidth", 0);
|
|
12530
13527
|
__publicField(this, "framebufferHeight", 0);
|
|
13528
|
+
// Blit shader for drawing render texture to screen
|
|
12531
13529
|
__publicField(this, "blitShaderProgram", null);
|
|
12532
13530
|
__publicField(this, "blitUniformLocations", { offset: null, scale: null, texture: null });
|
|
12533
13531
|
__publicField(this, "blitAttributeLocations", { position: 0, texCoord: 0 });
|
|
@@ -12548,11 +13546,15 @@ class WebGLRenderer {
|
|
|
12548
13546
|
this.splatBufferSize = 0;
|
|
12549
13547
|
this.alpha = alpha;
|
|
12550
13548
|
}
|
|
13549
|
+
/**
|
|
13550
|
+
* 初始化 WebGL 渲染器
|
|
13551
|
+
*/
|
|
12551
13552
|
async initialize() {
|
|
12552
13553
|
try {
|
|
12553
13554
|
this.gl = this.canvas.getContext("webgl2", {
|
|
12554
13555
|
antialias: false,
|
|
12555
13556
|
alpha: this.alpha,
|
|
13557
|
+
// 根据 isOpaque 设置透明度
|
|
12556
13558
|
premultipliedAlpha: true,
|
|
12557
13559
|
powerPreference: "high-performance",
|
|
12558
13560
|
preserveDrawingBuffer: false
|
|
@@ -12572,6 +13574,9 @@ class WebGLRenderer {
|
|
|
12572
13574
|
throw error;
|
|
12573
13575
|
}
|
|
12574
13576
|
}
|
|
13577
|
+
/**
|
|
13578
|
+
* 设置着色器位置
|
|
13579
|
+
*/
|
|
12575
13580
|
setupShaderLocations() {
|
|
12576
13581
|
const gl = this.gl;
|
|
12577
13582
|
if (!gl)
|
|
@@ -12587,12 +13592,20 @@ class WebGLRenderer {
|
|
|
12587
13592
|
};
|
|
12588
13593
|
this.attributeLocations = {
|
|
12589
13594
|
quadVertex: 0,
|
|
13595
|
+
// a_quadVertex (共享四边形顶点)
|
|
12590
13596
|
position: 1,
|
|
13597
|
+
// a_position (实例化)
|
|
12591
13598
|
color: 2,
|
|
13599
|
+
// a_color (实例化)
|
|
12592
13600
|
covA: 3,
|
|
13601
|
+
// a_covA (实例化)
|
|
12593
13602
|
covB: 4
|
|
13603
|
+
// a_covB (实例化)
|
|
12594
13604
|
};
|
|
12595
13605
|
}
|
|
13606
|
+
/**
|
|
13607
|
+
* 设置 WebGL 渲染状态
|
|
13608
|
+
*/
|
|
12596
13609
|
setupWebGLState() {
|
|
12597
13610
|
const gl = this.gl;
|
|
12598
13611
|
if (!gl)
|
|
@@ -12608,6 +13621,9 @@ class WebGLRenderer {
|
|
|
12608
13621
|
this.backgroundColor[3]
|
|
12609
13622
|
);
|
|
12610
13623
|
}
|
|
13624
|
+
/**
|
|
13625
|
+
* 创建渲染缓冲区
|
|
13626
|
+
*/
|
|
12611
13627
|
createBuffers() {
|
|
12612
13628
|
const gl = this.gl;
|
|
12613
13629
|
if (!gl)
|
|
@@ -12616,6 +13632,9 @@ class WebGLRenderer {
|
|
|
12616
13632
|
this.splatBuffer = gl.createBuffer();
|
|
12617
13633
|
this.createQuadVertexBuffer();
|
|
12618
13634
|
}
|
|
13635
|
+
/**
|
|
13636
|
+
* 创建四边形顶点缓冲区(实例化渲染用)
|
|
13637
|
+
*/
|
|
12619
13638
|
createQuadVertexBuffer() {
|
|
12620
13639
|
const gl = this.gl;
|
|
12621
13640
|
if (!gl)
|
|
@@ -12623,17 +13642,29 @@ class WebGLRenderer {
|
|
|
12623
13642
|
const quadVertices = new Float32Array([
|
|
12624
13643
|
-1,
|
|
12625
13644
|
-1,
|
|
13645
|
+
// 左下
|
|
12626
13646
|
-1,
|
|
12627
13647
|
1,
|
|
13648
|
+
// 左上
|
|
12628
13649
|
1,
|
|
12629
13650
|
-1,
|
|
13651
|
+
// 右下
|
|
12630
13652
|
1,
|
|
12631
13653
|
1
|
|
13654
|
+
// 右上
|
|
12632
13655
|
]);
|
|
12633
13656
|
this.quadVertexBuffer = gl.createBuffer();
|
|
12634
13657
|
gl.bindBuffer(gl.ARRAY_BUFFER, this.quadVertexBuffer);
|
|
12635
13658
|
gl.bufferData(gl.ARRAY_BUFFER, quadVertices, gl.STATIC_DRAW);
|
|
12636
13659
|
}
|
|
13660
|
+
/**
|
|
13661
|
+
* 从已打包数据加载(零拷贝,GPU 优化路径)
|
|
13662
|
+
* 🚀 性能优化版本:直接使用 WASM 输出的 packed 数据
|
|
13663
|
+
* 🚀 Buffer 复用:避免每帧重新分配,使用 bufferSubData 更新
|
|
13664
|
+
* @param packedData Float32Array [pos3, color4, cov6] x N 个点
|
|
13665
|
+
* @param pointCount 点数
|
|
13666
|
+
* @param _sortOrder WebGL 忽略此参数(已在 RenderSystem 中重排序)
|
|
13667
|
+
*/
|
|
12637
13668
|
loadSplatsFromPackedData(packedData, pointCount, _sortOrder) {
|
|
12638
13669
|
if (!this.isInitialized) {
|
|
12639
13670
|
throw new Error("Renderer not initialized");
|
|
@@ -12641,6 +13672,9 @@ class WebGLRenderer {
|
|
|
12641
13672
|
this.splatCount = pointCount;
|
|
12642
13673
|
this.uploadToGPU(packedData);
|
|
12643
13674
|
}
|
|
13675
|
+
/**
|
|
13676
|
+
* 上传数据到 GPU
|
|
13677
|
+
*/
|
|
12644
13678
|
uploadToGPU(packedData) {
|
|
12645
13679
|
const gl = this.gl;
|
|
12646
13680
|
if (!gl)
|
|
@@ -12653,6 +13687,9 @@ class WebGLRenderer {
|
|
|
12653
13687
|
gl.bufferSubData(gl.ARRAY_BUFFER, 0, packedData);
|
|
12654
13688
|
}
|
|
12655
13689
|
}
|
|
13690
|
+
/**
|
|
13691
|
+
* 设置实例化渲染顶点属性
|
|
13692
|
+
*/
|
|
12656
13693
|
setupVertexAttributes() {
|
|
12657
13694
|
const gl = this.gl;
|
|
12658
13695
|
if (!gl)
|
|
@@ -12713,6 +13750,9 @@ class WebGLRenderer {
|
|
|
12713
13750
|
);
|
|
12714
13751
|
gl.vertexAttribDivisor(this.attributeLocations.covB, 1);
|
|
12715
13752
|
}
|
|
13753
|
+
/**
|
|
13754
|
+
* 渲染一帧
|
|
13755
|
+
*/
|
|
12716
13756
|
render(viewMatrix, projectionMatrix, screenSize, transform) {
|
|
12717
13757
|
if (!this.isInitialized || this.splatCount === 0) {
|
|
12718
13758
|
return;
|
|
@@ -12736,6 +13776,9 @@ class WebGLRenderer {
|
|
|
12736
13776
|
this.render3DGS(gl, viewMatrix, projectionMatrix, screenSize, width, height);
|
|
12737
13777
|
}
|
|
12738
13778
|
}
|
|
13779
|
+
/**
|
|
13780
|
+
* 创建着色器程序
|
|
13781
|
+
*/
|
|
12739
13782
|
createShaderProgram(gl) {
|
|
12740
13783
|
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
|
|
12741
13784
|
if (!vertexShader)
|
|
@@ -12780,6 +13823,9 @@ class WebGLRenderer {
|
|
|
12780
13823
|
gl.deleteShader(fragmentShader);
|
|
12781
13824
|
return program;
|
|
12782
13825
|
}
|
|
13826
|
+
/**
|
|
13827
|
+
* 更新背景颜色
|
|
13828
|
+
*/
|
|
12783
13829
|
updateBackgroundColor(backgroundColor) {
|
|
12784
13830
|
this.backgroundColor = backgroundColor;
|
|
12785
13831
|
if (this.gl) {
|
|
@@ -12791,6 +13837,9 @@ class WebGLRenderer {
|
|
|
12791
13837
|
);
|
|
12792
13838
|
}
|
|
12793
13839
|
}
|
|
13840
|
+
/**
|
|
13841
|
+
* 渲染 3DGS 场景(公共方法,用于直接渲染和渲染到 framebuffer)
|
|
13842
|
+
*/
|
|
12794
13843
|
render3DGS(gl, viewMatrix, projectionMatrix, screenSize, width, height) {
|
|
12795
13844
|
gl.viewport(0, 0, width, height);
|
|
12796
13845
|
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
|
|
@@ -12806,6 +13855,9 @@ class WebGLRenderer {
|
|
|
12806
13855
|
this.setupVertexAttributes();
|
|
12807
13856
|
gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, this.splatCount);
|
|
12808
13857
|
}
|
|
13858
|
+
/**
|
|
13859
|
+
* 创建 framebuffer 和 render texture
|
|
13860
|
+
*/
|
|
12809
13861
|
createFramebuffer(width, height) {
|
|
12810
13862
|
const gl = this.gl;
|
|
12811
13863
|
if (!gl)
|
|
@@ -12848,6 +13900,9 @@ class WebGLRenderer {
|
|
|
12848
13900
|
this.framebufferWidth = width;
|
|
12849
13901
|
this.framebufferHeight = height;
|
|
12850
13902
|
}
|
|
13903
|
+
/**
|
|
13904
|
+
* 创建 blit shader(用于绘制 render texture 到屏幕)
|
|
13905
|
+
*/
|
|
12851
13906
|
createBlitShader(gl) {
|
|
12852
13907
|
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
|
|
12853
13908
|
if (!vertexShader)
|
|
@@ -12905,22 +13960,27 @@ class WebGLRenderer {
|
|
|
12905
13960
|
this.blitVAO = gl.createVertexArray();
|
|
12906
13961
|
gl.bindVertexArray(this.blitVAO);
|
|
12907
13962
|
const quadData = new Float32Array([
|
|
13963
|
+
// position (x, y), texCoord (u, v)
|
|
12908
13964
|
-1,
|
|
12909
13965
|
-1,
|
|
12910
13966
|
0,
|
|
12911
13967
|
0,
|
|
13968
|
+
// 左下
|
|
12912
13969
|
-1,
|
|
12913
13970
|
1,
|
|
12914
13971
|
0,
|
|
12915
13972
|
1,
|
|
13973
|
+
// 左上
|
|
12916
13974
|
1,
|
|
12917
13975
|
-1,
|
|
12918
13976
|
1,
|
|
12919
13977
|
0,
|
|
13978
|
+
// 右下
|
|
12920
13979
|
1,
|
|
12921
13980
|
1,
|
|
12922
13981
|
1,
|
|
12923
13982
|
1
|
|
13983
|
+
// 右上
|
|
12924
13984
|
]);
|
|
12925
13985
|
this.blitQuadBuffer = gl.createBuffer();
|
|
12926
13986
|
gl.bindBuffer(gl.ARRAY_BUFFER, this.blitQuadBuffer);
|
|
@@ -12931,6 +13991,9 @@ class WebGLRenderer {
|
|
|
12931
13991
|
gl.vertexAttribPointer(this.blitAttributeLocations.texCoord, 2, gl.FLOAT, false, 16, 8);
|
|
12932
13992
|
gl.bindVertexArray(null);
|
|
12933
13993
|
}
|
|
13994
|
+
/**
|
|
13995
|
+
* 将 render texture 绘制到屏幕(应用 transform)
|
|
13996
|
+
*/
|
|
12934
13997
|
blitToScreen(transform) {
|
|
12935
13998
|
const gl = this.gl;
|
|
12936
13999
|
if (!gl || !this.blitShaderProgram || !this.renderTexture || !this.blitVAO) {
|
|
@@ -12959,6 +14022,9 @@ class WebGLRenderer {
|
|
|
12959
14022
|
gl.enable(gl.BLEND);
|
|
12960
14023
|
}
|
|
12961
14024
|
}
|
|
14025
|
+
/**
|
|
14026
|
+
* 清理资源
|
|
14027
|
+
*/
|
|
12962
14028
|
dispose() {
|
|
12963
14029
|
if (!this.gl)
|
|
12964
14030
|
return;
|
|
@@ -13006,21 +14072,26 @@ class WebGPURenderer {
|
|
|
13006
14072
|
__publicField(this, "context", null);
|
|
13007
14073
|
__publicField(this, "renderPipeline", null);
|
|
13008
14074
|
__publicField(this, "renderTexturePipeline", null);
|
|
14075
|
+
// 用于渲染到 render texture
|
|
13009
14076
|
__publicField(this, "quadVertexBuffer", null);
|
|
13010
14077
|
__publicField(this, "uniformBuffer", null);
|
|
13011
14078
|
__publicField(this, "uniformBindGroup", null);
|
|
14079
|
+
// 🚀 间接索引渲染 buffers
|
|
13012
14080
|
__publicField(this, "sortIndexBuffer", null);
|
|
13013
14081
|
__publicField(this, "splatDataBuffer", null);
|
|
13014
14082
|
__publicField(this, "storageBindGroup", null);
|
|
13015
14083
|
__publicField(this, "bindGroupNeedsUpdate", false);
|
|
14084
|
+
// 标记 bind group 是否需要更新
|
|
13016
14085
|
__publicField(this, "splatCount", 0);
|
|
13017
14086
|
__publicField(this, "presentationFormat", "bgra8unorm");
|
|
13018
14087
|
__publicField(this, "alpha");
|
|
14088
|
+
// Render texture framebuffer
|
|
13019
14089
|
__publicField(this, "renderTexture", null);
|
|
13020
14090
|
__publicField(this, "renderTextureView", null);
|
|
13021
14091
|
__publicField(this, "depthTexture", null);
|
|
13022
14092
|
__publicField(this, "framebufferWidth", 0);
|
|
13023
14093
|
__publicField(this, "framebufferHeight", 0);
|
|
14094
|
+
// Blit pipeline for drawing render texture to screen
|
|
13024
14095
|
__publicField(this, "blitPipeline", null);
|
|
13025
14096
|
__publicField(this, "blitUniformBuffer", null);
|
|
13026
14097
|
__publicField(this, "blitQuadBuffer", null);
|
|
@@ -13029,6 +14100,9 @@ class WebGPURenderer {
|
|
|
13029
14100
|
this.backgroundColor = backgroundColor || [0, 0, 0, 0];
|
|
13030
14101
|
this.alpha = alpha;
|
|
13031
14102
|
}
|
|
14103
|
+
/**
|
|
14104
|
+
* 初始化 WebGPU 渲染器
|
|
14105
|
+
*/
|
|
13032
14106
|
async initialize() {
|
|
13033
14107
|
const adapter = await navigator.gpu.requestAdapter({
|
|
13034
14108
|
powerPreference: "high-performance"
|
|
@@ -13052,6 +14126,9 @@ class WebGPURenderer {
|
|
|
13052
14126
|
await this.createRenderPipeline();
|
|
13053
14127
|
await this.createBlitPipeline();
|
|
13054
14128
|
}
|
|
14129
|
+
/**
|
|
14130
|
+
* 创建 Uniform Buffer
|
|
14131
|
+
*/
|
|
13055
14132
|
createUniformBuffer() {
|
|
13056
14133
|
if (!this.device)
|
|
13057
14134
|
return;
|
|
@@ -13062,18 +14139,25 @@ class WebGPURenderer {
|
|
|
13062
14139
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
13063
14140
|
});
|
|
13064
14141
|
}
|
|
14142
|
+
/**
|
|
14143
|
+
* 创建四边形顶点缓冲区(实例化渲染用)
|
|
14144
|
+
*/
|
|
13065
14145
|
createQuadVertexBuffer() {
|
|
13066
14146
|
if (!this.device)
|
|
13067
14147
|
return;
|
|
13068
14148
|
const quadVertices = new Float32Array([
|
|
13069
14149
|
-1,
|
|
13070
14150
|
-1,
|
|
14151
|
+
// 左下
|
|
13071
14152
|
-1,
|
|
13072
14153
|
1,
|
|
14154
|
+
// 左上
|
|
13073
14155
|
1,
|
|
13074
14156
|
-1,
|
|
14157
|
+
// 右下
|
|
13075
14158
|
1,
|
|
13076
14159
|
1
|
|
14160
|
+
// 右上
|
|
13077
14161
|
]);
|
|
13078
14162
|
this.quadVertexBuffer = this.device.createBuffer({
|
|
13079
14163
|
label: "Quad Vertex Buffer",
|
|
@@ -13084,6 +14168,9 @@ class WebGPURenderer {
|
|
|
13084
14168
|
new Float32Array(this.quadVertexBuffer.getMappedRange()).set(quadVertices);
|
|
13085
14169
|
this.quadVertexBuffer.unmap();
|
|
13086
14170
|
}
|
|
14171
|
+
/**
|
|
14172
|
+
* 创建 Render Pipeline
|
|
14173
|
+
*/
|
|
13087
14174
|
async createRenderPipeline() {
|
|
13088
14175
|
if (!this.device)
|
|
13089
14176
|
return;
|
|
@@ -13106,11 +14193,13 @@ class WebGPURenderer {
|
|
|
13106
14193
|
entries: [
|
|
13107
14194
|
{
|
|
13108
14195
|
binding: 0,
|
|
14196
|
+
// sortIndices
|
|
13109
14197
|
visibility: GPUShaderStage.VERTEX,
|
|
13110
14198
|
buffer: { type: "read-only-storage" }
|
|
13111
14199
|
},
|
|
13112
14200
|
{
|
|
13113
14201
|
binding: 1,
|
|
14202
|
+
// splatData
|
|
13114
14203
|
visibility: GPUShaderStage.VERTEX,
|
|
13115
14204
|
buffer: { type: "read-only-storage" }
|
|
13116
14205
|
}
|
|
@@ -13121,12 +14210,15 @@ class WebGPURenderer {
|
|
|
13121
14210
|
bindGroupLayouts: [uniformBindGroupLayout, storageBindGroupLayout]
|
|
13122
14211
|
});
|
|
13123
14212
|
const vertexBufferLayouts = [
|
|
14213
|
+
// Buffer 0: Quad vertices (per-vertex)
|
|
13124
14214
|
{
|
|
13125
14215
|
arrayStride: 8,
|
|
14216
|
+
// 2 floats
|
|
13126
14217
|
stepMode: "vertex",
|
|
13127
14218
|
attributes: [
|
|
13128
14219
|
{
|
|
13129
14220
|
shaderLocation: 0,
|
|
14221
|
+
// quadVertex
|
|
13130
14222
|
offset: 0,
|
|
13131
14223
|
format: "float32x2"
|
|
13132
14224
|
}
|
|
@@ -13148,6 +14240,7 @@ class WebGPURenderer {
|
|
|
13148
14240
|
{
|
|
13149
14241
|
format: this.presentationFormat,
|
|
13150
14242
|
blend: {
|
|
14243
|
+
// 预乘 alpha 混合(匹配 alphaMode: 'premultiplied')
|
|
13151
14244
|
color: {
|
|
13152
14245
|
srcFactor: "one",
|
|
13153
14246
|
dstFactor: "one-minus-src-alpha",
|
|
@@ -13167,6 +14260,7 @@ class WebGPURenderer {
|
|
|
13167
14260
|
stripIndexFormat: void 0
|
|
13168
14261
|
},
|
|
13169
14262
|
depthStencil: void 0
|
|
14263
|
+
// 3DGS 不使用深度测试
|
|
13170
14264
|
});
|
|
13171
14265
|
this.uniformBindGroup = this.device.createBindGroup({
|
|
13172
14266
|
label: "Uniform Bind Group",
|
|
@@ -13192,6 +14286,7 @@ class WebGPURenderer {
|
|
|
13192
14286
|
targets: [
|
|
13193
14287
|
{
|
|
13194
14288
|
format: "rgba16float",
|
|
14289
|
+
// 使用 16-bit float 格式
|
|
13195
14290
|
blend: {
|
|
13196
14291
|
color: {
|
|
13197
14292
|
srcFactor: "one",
|
|
@@ -13214,10 +14309,15 @@ class WebGPURenderer {
|
|
|
13214
14309
|
depthStencil: {
|
|
13215
14310
|
format: "depth24plus",
|
|
13216
14311
|
depthWriteEnabled: false,
|
|
14312
|
+
// 不写入深度
|
|
13217
14313
|
depthCompare: "always"
|
|
14314
|
+
// 总是通过深度测试(实际上不使用)
|
|
13218
14315
|
}
|
|
13219
14316
|
});
|
|
13220
14317
|
}
|
|
14318
|
+
/**
|
|
14319
|
+
* 创建 Blit Pipeline(用于绘制 render texture 到屏幕)
|
|
14320
|
+
*/
|
|
13221
14321
|
async createBlitPipeline() {
|
|
13222
14322
|
if (!this.device)
|
|
13223
14323
|
return;
|
|
@@ -13235,6 +14335,7 @@ class WebGPURenderer {
|
|
|
13235
14335
|
entries: [
|
|
13236
14336
|
{
|
|
13237
14337
|
binding: 0,
|
|
14338
|
+
// Uniforms
|
|
13238
14339
|
visibility: GPUShaderStage.VERTEX,
|
|
13239
14340
|
buffer: { type: "uniform" }
|
|
13240
14341
|
}
|
|
@@ -13245,11 +14346,13 @@ class WebGPURenderer {
|
|
|
13245
14346
|
entries: [
|
|
13246
14347
|
{
|
|
13247
14348
|
binding: 0,
|
|
14349
|
+
// Texture
|
|
13248
14350
|
visibility: GPUShaderStage.FRAGMENT,
|
|
13249
14351
|
texture: {}
|
|
13250
14352
|
},
|
|
13251
14353
|
{
|
|
13252
14354
|
binding: 1,
|
|
14355
|
+
// Sampler
|
|
13253
14356
|
visibility: GPUShaderStage.FRAGMENT,
|
|
13254
14357
|
sampler: {}
|
|
13255
14358
|
}
|
|
@@ -13265,22 +14368,27 @@ class WebGPURenderer {
|
|
|
13265
14368
|
minFilter: "linear"
|
|
13266
14369
|
});
|
|
13267
14370
|
const quadData = new Float32Array([
|
|
14371
|
+
// position (x, y), texCoord (u, v)
|
|
13268
14372
|
-1,
|
|
13269
14373
|
-1,
|
|
13270
14374
|
0,
|
|
13271
14375
|
0,
|
|
14376
|
+
// 左下
|
|
13272
14377
|
-1,
|
|
13273
14378
|
1,
|
|
13274
14379
|
0,
|
|
13275
14380
|
1,
|
|
14381
|
+
// 左上
|
|
13276
14382
|
1,
|
|
13277
14383
|
-1,
|
|
13278
14384
|
1,
|
|
13279
14385
|
0,
|
|
14386
|
+
// 右下
|
|
13280
14387
|
1,
|
|
13281
14388
|
1,
|
|
13282
14389
|
1,
|
|
13283
14390
|
1
|
|
14391
|
+
// 右上
|
|
13284
14392
|
]);
|
|
13285
14393
|
this.blitQuadBuffer = this.device.createBuffer({
|
|
13286
14394
|
label: "Blit Quad Buffer",
|
|
@@ -13299,6 +14407,7 @@ class WebGPURenderer {
|
|
|
13299
14407
|
buffers: [
|
|
13300
14408
|
{
|
|
13301
14409
|
arrayStride: 16,
|
|
14410
|
+
// 4 floats (2 position + 2 texCoord)
|
|
13302
14411
|
stepMode: "vertex",
|
|
13303
14412
|
attributes: [
|
|
13304
14413
|
{
|
|
@@ -13321,6 +14430,7 @@ class WebGPURenderer {
|
|
|
13321
14430
|
targets: [
|
|
13322
14431
|
{
|
|
13323
14432
|
format: this.presentationFormat,
|
|
14433
|
+
// render texture 已经是最终渲染结果,直接覆盖,不需要混合
|
|
13324
14434
|
blend: void 0
|
|
13325
14435
|
}
|
|
13326
14436
|
]
|
|
@@ -13330,6 +14440,9 @@ class WebGPURenderer {
|
|
|
13330
14440
|
}
|
|
13331
14441
|
});
|
|
13332
14442
|
}
|
|
14443
|
+
/**
|
|
14444
|
+
* 创建 render texture 和 depth texture
|
|
14445
|
+
*/
|
|
13333
14446
|
createRenderTexture(width, height) {
|
|
13334
14447
|
if (!this.device)
|
|
13335
14448
|
return;
|
|
@@ -13343,6 +14456,7 @@ class WebGPURenderer {
|
|
|
13343
14456
|
label: "Render Texture",
|
|
13344
14457
|
size: [width, height],
|
|
13345
14458
|
format: "rgba16float",
|
|
14459
|
+
// 使用 16-bit float 以保持精度
|
|
13346
14460
|
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING
|
|
13347
14461
|
});
|
|
13348
14462
|
this.renderTextureView = this.renderTexture.createView();
|
|
@@ -13355,6 +14469,11 @@ class WebGPURenderer {
|
|
|
13355
14469
|
this.framebufferWidth = width;
|
|
13356
14470
|
this.framebufferHeight = height;
|
|
13357
14471
|
}
|
|
14472
|
+
/**
|
|
14473
|
+
* 从已打包数据加载
|
|
14474
|
+
* 🚀 间接索引渲染:packedData 是原始数据,sortOrder 是排序索引
|
|
14475
|
+
* 🚀 完全消除 CPU 重排序开销
|
|
14476
|
+
*/
|
|
13358
14477
|
loadSplatsFromPackedData(packedData, pointCount, sortOrder) {
|
|
13359
14478
|
if (!this.device)
|
|
13360
14479
|
throw new Error("Device not initialized");
|
|
@@ -13417,6 +14536,9 @@ class WebGPURenderer {
|
|
|
13417
14536
|
}
|
|
13418
14537
|
}
|
|
13419
14538
|
}
|
|
14539
|
+
/**
|
|
14540
|
+
* 渲染一帧
|
|
14541
|
+
*/
|
|
13420
14542
|
render(viewMatrix, projectionMatrix, screenSize, transform) {
|
|
13421
14543
|
if (!this.device || !this.context || !this.renderPipeline)
|
|
13422
14544
|
return;
|
|
@@ -13488,6 +14610,9 @@ class WebGPURenderer {
|
|
|
13488
14610
|
}
|
|
13489
14611
|
this.device.queue.submit([commandEncoder.finish()]);
|
|
13490
14612
|
}
|
|
14613
|
+
/**
|
|
14614
|
+
* 将 render texture 绘制到屏幕(应用 transform)
|
|
14615
|
+
*/
|
|
13491
14616
|
blitToScreen(commandEncoder, transform) {
|
|
13492
14617
|
if (!this.device || !this.blitPipeline || !this.renderTextureView || !this.blitQuadBuffer || !this.blitUniformBuffer || !this.blitSampler) {
|
|
13493
14618
|
logger.error(`[WebGPURenderer] Blit failed: device=${!!this.device}, pipeline=${!!this.blitPipeline}, texture=${!!this.renderTextureView}, buffer=${!!this.blitQuadBuffer}, uniform=${!!this.blitUniformBuffer}, sampler=${!!this.blitSampler}`);
|
|
@@ -13551,6 +14676,9 @@ class WebGPURenderer {
|
|
|
13551
14676
|
blitPass.draw(4);
|
|
13552
14677
|
blitPass.end();
|
|
13553
14678
|
}
|
|
14679
|
+
/**
|
|
14680
|
+
* 更新 Uniform Buffer
|
|
14681
|
+
*/
|
|
13554
14682
|
updateUniforms(viewMatrix, projectionMatrix, screenSize) {
|
|
13555
14683
|
if (!this.device || !this.uniformBuffer)
|
|
13556
14684
|
return;
|
|
@@ -13564,9 +14692,15 @@ class WebGPURenderer {
|
|
|
13564
14692
|
uint32View[36] = 1;
|
|
13565
14693
|
this.device.queue.writeBuffer(this.uniformBuffer, 0, uniformData);
|
|
13566
14694
|
}
|
|
14695
|
+
/**
|
|
14696
|
+
* 更新背景颜色
|
|
14697
|
+
*/
|
|
13567
14698
|
updateBackgroundColor(backgroundColor) {
|
|
13568
14699
|
this.backgroundColor = backgroundColor;
|
|
13569
14700
|
}
|
|
14701
|
+
/**
|
|
14702
|
+
* 清理资源
|
|
14703
|
+
*/
|
|
13570
14704
|
dispose() {
|
|
13571
14705
|
var _a, _b, _c, _d, _e2, _f, _g, _h, _i2;
|
|
13572
14706
|
(_a = this.sortIndexBuffer) == null ? void 0 : _a.destroy();
|
|
@@ -13604,19 +14738,29 @@ class RenderSystem {
|
|
|
13604
14738
|
__publicField(this, "canvas");
|
|
13605
14739
|
__publicField(this, "options");
|
|
13606
14740
|
__publicField(this, "backgroundColor");
|
|
14741
|
+
// Camera configuration
|
|
13607
14742
|
__publicField(this, "camera");
|
|
14743
|
+
// Matrix cache (reuse buffers to avoid per-frame allocation)
|
|
13608
14744
|
__publicField(this, "viewMatrix", new Float32Array(16));
|
|
13609
14745
|
__publicField(this, "projectionMatrix", new Float32Array(16));
|
|
14746
|
+
// Camera forward vector cache (reuse to avoid per-frame allocation)
|
|
13610
14747
|
__publicField(this, "cachedForward", [0, 0, 1]);
|
|
13611
14748
|
__publicField(this, "forwardCacheValid", false);
|
|
14749
|
+
// Temporary vectors for view matrix calculation (reuse buffers)
|
|
13612
14750
|
__publicField(this, "tempForward", [0, 0, 0]);
|
|
13613
14751
|
__publicField(this, "tempRight", [0, 0, 0]);
|
|
13614
14752
|
__publicField(this, "tempUp", [0, 0, 0]);
|
|
14753
|
+
// Camera parameter cache for matrix update optimization
|
|
13615
14754
|
__publicField(this, "cachedCameraParams", null);
|
|
13616
14755
|
__publicField(this, "matricesCacheValid", false);
|
|
14756
|
+
// 当前数据(GPU 格式:[pos3, color4, cov6] x N)
|
|
13617
14757
|
__publicField(this, "originalPackedData", null);
|
|
14758
|
+
// 性能统计
|
|
13618
14759
|
__publicField(this, "renderTime", 0);
|
|
14760
|
+
// 总渲染耗时
|
|
13619
14761
|
__publicField(this, "sortTime", 0);
|
|
14762
|
+
// 排序耗时
|
|
14763
|
+
// Transform for render texture blit
|
|
13620
14764
|
__publicField(this, "offsetX", 0);
|
|
13621
14765
|
__publicField(this, "offsetY", 0);
|
|
13622
14766
|
__publicField(this, "scale", 1);
|
|
@@ -13636,6 +14780,9 @@ class RenderSystem {
|
|
|
13636
14780
|
aspect: 1
|
|
13637
14781
|
};
|
|
13638
14782
|
}
|
|
14783
|
+
/**
|
|
14784
|
+
* Initialize render system
|
|
14785
|
+
*/
|
|
13639
14786
|
async initialize() {
|
|
13640
14787
|
const { preferBackend, alpha = true } = this.options;
|
|
13641
14788
|
const backgroundColor = this.backgroundColor;
|
|
@@ -13660,11 +14807,20 @@ class RenderSystem {
|
|
|
13660
14807
|
logger.log("✅ Using WebGL renderer");
|
|
13661
14808
|
this.updateCameraAspect();
|
|
13662
14809
|
}
|
|
14810
|
+
/**
|
|
14811
|
+
* Load packed Splat data (zero-copy, GPU format)
|
|
14812
|
+
* Directly receives WASM packed data
|
|
14813
|
+
*
|
|
14814
|
+
* @param packedData Float32Array [pos3, color4, cov6] x N points
|
|
14815
|
+
*/
|
|
13663
14816
|
loadSplatsFromPackedData(packedData) {
|
|
13664
14817
|
if (!this.renderer)
|
|
13665
14818
|
throw new Error("Renderer not initialized");
|
|
13666
14819
|
this.originalPackedData = packedData;
|
|
13667
14820
|
}
|
|
14821
|
+
/**
|
|
14822
|
+
* 渲染一帧
|
|
14823
|
+
*/
|
|
13668
14824
|
renderFrame() {
|
|
13669
14825
|
if (!this.renderer || !this.originalPackedData)
|
|
13670
14826
|
return;
|
|
@@ -13689,43 +14845,72 @@ class RenderSystem {
|
|
|
13689
14845
|
this.viewMatrix,
|
|
13690
14846
|
this.projectionMatrix,
|
|
13691
14847
|
[this.canvas.width, this.canvas.height],
|
|
14848
|
+
// 传递 transform(如果不需要 transform,则不传递,保持向后兼容)
|
|
13692
14849
|
this.offsetX !== 0 || this.offsetY !== 0 || this.scale !== 1 ? { x: this.offsetX, y: this.offsetY, scale: this.scale } : void 0
|
|
13693
14850
|
);
|
|
13694
14851
|
const renderTime = performance.now() - startRender;
|
|
13695
14852
|
this.renderTime = renderTime;
|
|
13696
14853
|
}
|
|
14854
|
+
/**
|
|
14855
|
+
* Set transform for render texture blit
|
|
14856
|
+
* @param x - Horizontal offset in normalized coordinates (-1 to 1, where -1 = left edge, 0 = center, 1 = right edge)
|
|
14857
|
+
* @param y - Vertical offset in normalized coordinates (-1 to 1, where -1 = bottom edge, 0 = center, 1 = top edge)
|
|
14858
|
+
* @param scale - Scale factor (1.0 = original size, 2.0 = double size, 0.5 = half size)
|
|
14859
|
+
*/
|
|
13697
14860
|
setTransform(x2, y2, scale = 1) {
|
|
13698
14861
|
logger.log(`[RenderSystem] Setting transform: x=${x2}, y=${y2}, scale=${scale}`);
|
|
13699
14862
|
this.offsetX = x2;
|
|
13700
14863
|
this.offsetY = y2;
|
|
13701
14864
|
this.scale = scale;
|
|
13702
14865
|
}
|
|
14866
|
+
/**
|
|
14867
|
+
* Get current transform
|
|
14868
|
+
*/
|
|
13703
14869
|
getTransform() {
|
|
13704
14870
|
return { x: this.offsetX, y: this.offsetY, scale: this.scale };
|
|
13705
14871
|
}
|
|
14872
|
+
/**
|
|
14873
|
+
* Update camera parameters
|
|
14874
|
+
*/
|
|
13706
14875
|
updateCamera(params) {
|
|
13707
14876
|
Object.assign(this.camera, params);
|
|
13708
14877
|
this.updateCameraAspect();
|
|
13709
14878
|
}
|
|
14879
|
+
/**
|
|
14880
|
+
* Handle window resize
|
|
14881
|
+
*/
|
|
13710
14882
|
handleResize() {
|
|
13711
14883
|
this.updateCameraAspect();
|
|
13712
14884
|
this.updateCameraMatrices();
|
|
13713
14885
|
}
|
|
14886
|
+
/**
|
|
14887
|
+
* 获取当前使用的后端
|
|
14888
|
+
*/
|
|
13714
14889
|
getBackend() {
|
|
13715
14890
|
return this.backend;
|
|
13716
14891
|
}
|
|
14892
|
+
/**
|
|
14893
|
+
* 更新背景颜色
|
|
14894
|
+
*/
|
|
13717
14895
|
updateBackgroundColor(backgroundColor) {
|
|
13718
14896
|
this.backgroundColor = backgroundColor;
|
|
13719
14897
|
if (this.renderer && typeof this.renderer.updateBackgroundColor === "function") {
|
|
13720
14898
|
this.renderer.updateBackgroundColor(backgroundColor);
|
|
13721
14899
|
}
|
|
13722
14900
|
}
|
|
14901
|
+
/**
|
|
14902
|
+
* Dispose resources
|
|
14903
|
+
*/
|
|
13723
14904
|
dispose() {
|
|
13724
14905
|
var _a;
|
|
13725
14906
|
(_a = this.renderer) == null ? void 0 : _a.dispose();
|
|
13726
14907
|
this.renderer = null;
|
|
13727
14908
|
this.originalPackedData = null;
|
|
13728
14909
|
}
|
|
14910
|
+
// ========== Private Methods ==========
|
|
14911
|
+
/**
|
|
14912
|
+
* Check WebGPU support
|
|
14913
|
+
*/
|
|
13729
14914
|
async checkWebGPUSupport() {
|
|
13730
14915
|
if (!navigator.gpu)
|
|
13731
14916
|
return false;
|
|
@@ -13736,10 +14921,16 @@ class RenderSystem {
|
|
|
13736
14921
|
return false;
|
|
13737
14922
|
}
|
|
13738
14923
|
}
|
|
14924
|
+
/**
|
|
14925
|
+
* Update camera aspect ratio
|
|
14926
|
+
*/
|
|
13739
14927
|
updateCameraAspect() {
|
|
13740
14928
|
this.camera.aspect = this.canvas.width / this.canvas.height;
|
|
13741
14929
|
this.matricesCacheValid = false;
|
|
13742
14930
|
}
|
|
14931
|
+
/**
|
|
14932
|
+
* Update camera matrices (with caching to avoid unnecessary updates)
|
|
14933
|
+
*/
|
|
13743
14934
|
updateCameraMatrices() {
|
|
13744
14935
|
const { position, target, up, fov, aspect, near, far } = this.camera;
|
|
13745
14936
|
const paramsChanged = !this.cachedCameraParams || this.cachedCameraParams.position[0] !== position[0] || this.cachedCameraParams.position[1] !== position[1] || this.cachedCameraParams.position[2] !== position[2] || this.cachedCameraParams.target[0] !== target[0] || this.cachedCameraParams.target[1] !== target[1] || this.cachedCameraParams.target[2] !== target[2] || this.cachedCameraParams.up[0] !== up[0] || this.cachedCameraParams.up[1] !== up[1] || this.cachedCameraParams.up[2] !== up[2] || this.cachedCameraParams.fov !== fov || this.cachedCameraParams.aspect !== aspect || this.cachedCameraParams.near !== near || this.cachedCameraParams.far !== far;
|
|
@@ -13776,6 +14967,9 @@ class RenderSystem {
|
|
|
13776
14967
|
this.updateViewMatrix(position, target, up);
|
|
13777
14968
|
this.matricesCacheValid = true;
|
|
13778
14969
|
}
|
|
14970
|
+
/**
|
|
14971
|
+
* Get camera forward vector (cached version)
|
|
14972
|
+
*/
|
|
13779
14973
|
getCameraForward() {
|
|
13780
14974
|
if (this.forwardCacheValid) {
|
|
13781
14975
|
return this.cachedForward;
|
|
@@ -13794,6 +14988,9 @@ class RenderSystem {
|
|
|
13794
14988
|
this.forwardCacheValid = true;
|
|
13795
14989
|
return this.cachedForward;
|
|
13796
14990
|
}
|
|
14991
|
+
/**
|
|
14992
|
+
* Update perspective projection matrix (reuse buffer)
|
|
14993
|
+
*/
|
|
13797
14994
|
updatePerspectiveMatrix(fov, aspect, near, far) {
|
|
13798
14995
|
const fovYRadians = fov * Math.PI / 180;
|
|
13799
14996
|
const f2 = 1 / Math.tan(fovYRadians / 2);
|
|
@@ -13816,6 +15013,12 @@ class RenderSystem {
|
|
|
13816
15013
|
m2[14] = zs2 * near;
|
|
13817
15014
|
m2[15] = 0;
|
|
13818
15015
|
}
|
|
15016
|
+
/**
|
|
15017
|
+
* Update view matrix (directly update pre-allocated buffer to avoid per-frame allocation)
|
|
15018
|
+
* Equivalent to: inverse(translation) * inverse(rotation) = T^(-1) * R^(-1)
|
|
15019
|
+
* Where T is translation matrix, R is rotation matrix
|
|
15020
|
+
* Uses reusable temporary vector buffers to avoid allocations
|
|
15021
|
+
*/
|
|
13819
15022
|
updateViewMatrix(position, target, up) {
|
|
13820
15023
|
this.tempForward[0] = target[0] - position[0];
|
|
13821
15024
|
this.tempForward[1] = target[1] - position[1];
|
|
@@ -13925,17 +15128,26 @@ function createBezierEasing(x1, y1, x2, y2) {
|
|
|
13925
15128
|
};
|
|
13926
15129
|
}
|
|
13927
15130
|
const BEZIER_CURVES = {
|
|
15131
|
+
// jaw: 快速启动,平稳停止
|
|
13928
15132
|
jaw: createBezierEasing(0.2, 0.8, 0.3, 1),
|
|
15133
|
+
// expression: 平滑 S 曲线
|
|
13929
15134
|
expression: createBezierEasing(0.4, 0, 0.2, 1),
|
|
15135
|
+
// eye: 更柔和的 S 曲线
|
|
13930
15136
|
eye: createBezierEasing(0.3, 0, 0.1, 1),
|
|
15137
|
+
// neck: 慢启动,惯性停止
|
|
13931
15138
|
neck: createBezierEasing(0.1, 0.2, 0.2, 1),
|
|
15139
|
+
// global: 标准 ease-in-out
|
|
13932
15140
|
global: createBezierEasing(0.42, 0, 0.58, 1)
|
|
13933
15141
|
};
|
|
13934
15142
|
const TIME_SCALE = {
|
|
13935
15143
|
jaw: 2.5,
|
|
15144
|
+
// 40% 时间完成
|
|
13936
15145
|
expression: 1.6,
|
|
15146
|
+
// 62.5% 时间完成
|
|
13937
15147
|
eye: 1.3,
|
|
15148
|
+
// 77% 时间完成
|
|
13938
15149
|
neck: 1,
|
|
15150
|
+
// 100% 时间完成
|
|
13939
15151
|
global: 1
|
|
13940
15152
|
};
|
|
13941
15153
|
function bezierLerp(from, to2, progress) {
|
|
@@ -13975,35 +15187,55 @@ function generateTransitionFrames(from, to2, durationMs, fps = 25) {
|
|
|
13975
15187
|
return frames;
|
|
13976
15188
|
}
|
|
13977
15189
|
class AvatarView {
|
|
15190
|
+
/**
|
|
15191
|
+
* Constructor
|
|
15192
|
+
* Creates a unified AvatarController, internally composes network layer based on configuration
|
|
15193
|
+
* @param avatar - Avatar instance
|
|
15194
|
+
* @param container - Canvas container element (required)
|
|
15195
|
+
*/
|
|
13978
15196
|
constructor(avatar, container) {
|
|
13979
15197
|
__publicField(this, "avatarController");
|
|
13980
15198
|
__publicField(this, "avatar");
|
|
15199
|
+
// 首帧渲染回调
|
|
13981
15200
|
__publicField(this, "onFirstRendering");
|
|
15201
|
+
// Canvas and rendering
|
|
13982
15202
|
__publicField(this, "canvas");
|
|
13983
15203
|
__publicField(this, "renderSystem", null);
|
|
13984
15204
|
__publicField(this, "isInitialized", false);
|
|
13985
15205
|
__publicField(this, "cameraConfig", null);
|
|
15206
|
+
// Rendering state machine
|
|
13986
15207
|
__publicField(this, "renderingState", "idle");
|
|
15208
|
+
// Realtime animation data
|
|
13987
15209
|
__publicField(this, "currentKeyframes", []);
|
|
13988
15210
|
__publicField(this, "lastRenderedFrameIndex", -1);
|
|
13989
15211
|
__publicField(this, "lastRealtimeProtoFrame", null);
|
|
15212
|
+
// Animation loop types
|
|
13990
15213
|
__publicField(this, "idleAnimationLoopId", null);
|
|
13991
15214
|
__publicField(this, "realtimeAnimationLoopId", null);
|
|
13992
15215
|
__publicField(this, "resizeObserver", null);
|
|
13993
15216
|
__publicField(this, "onWindowResize", () => this.handleResize());
|
|
15217
|
+
// FPS 计算
|
|
13994
15218
|
__publicField(this, "frameCount", 0);
|
|
13995
15219
|
__publicField(this, "lastFpsUpdate", 0);
|
|
13996
15220
|
__publicField(this, "currentFPS", 0);
|
|
15221
|
+
// Transition animation data
|
|
13997
15222
|
__publicField(this, "transitionKeyframes", []);
|
|
13998
15223
|
__publicField(this, "transitionStartTime", 0);
|
|
13999
15224
|
__publicField(this, "startTransitionDurationMs", 200);
|
|
15225
|
+
// Idle -> Speaking 过渡时长
|
|
14000
15226
|
__publicField(this, "endTransitionDurationMs", 1600);
|
|
15227
|
+
// Speaking -> Idle 过渡时长
|
|
14001
15228
|
__publicField(this, "cachedIdleFirstFrame", null);
|
|
14002
15229
|
__publicField(this, "idleCurrentFrameIndex", 0);
|
|
15230
|
+
// 当前正在播放的帧(统一用于所有过渡,无论处于什么状态)
|
|
14003
15231
|
__publicField(this, "currentPlayingFrame", null);
|
|
15232
|
+
// Character handle for multi-character support
|
|
14004
15233
|
__publicField(this, "characterHandle", null);
|
|
14005
15234
|
__publicField(this, "characterId");
|
|
15235
|
+
// Unique ID for this character instance
|
|
15236
|
+
// 纯渲染模式标志(阻止 idle 循环渲染)
|
|
14006
15237
|
__publicField(this, "isPureRenderingMode", false);
|
|
15238
|
+
// avatar_active 埋点相关
|
|
14007
15239
|
__publicField(this, "avatarActiveTimer", null);
|
|
14008
15240
|
__publicField(this, "AVATAR_ACTIVE_INTERVAL", 6e5);
|
|
14009
15241
|
this.avatar = avatar;
|
|
@@ -14024,6 +15256,10 @@ class AvatarView {
|
|
|
14024
15256
|
});
|
|
14025
15257
|
this.setupControllerEventListeners();
|
|
14026
15258
|
}
|
|
15259
|
+
// 10分钟 = 600000ms
|
|
15260
|
+
/**
|
|
15261
|
+
* 对齐两端 Flame 维度:标量统一长度,expression 取最大长度并零填充
|
|
15262
|
+
*/
|
|
14027
15263
|
alignFlamePair(from, to2) {
|
|
14028
15264
|
const ensureLen = (arr, len) => {
|
|
14029
15265
|
const a2 = Array.isArray(arr) ? arr.slice(0, len) : [];
|
|
@@ -14055,6 +15291,13 @@ class AvatarView {
|
|
|
14055
15291
|
toFixed.expression = ensureLen(toFixed.expression, exprLen);
|
|
14056
15292
|
return { from: fromFixed, to: toFixed };
|
|
14057
15293
|
}
|
|
15294
|
+
/**
|
|
15295
|
+
* 生成并对齐过渡帧,确保首尾帧与起止帧完全一致
|
|
15296
|
+
* @param from 起始帧
|
|
15297
|
+
* @param to 目标帧
|
|
15298
|
+
* @param durationMs 过渡时长(开头或结尾)
|
|
15299
|
+
* @param useLinearInterpolation 是否使用线性插值(旧实现),true 用于 idle->speaking,false 用于 speaking->idle(使用 Bezier 曲线)
|
|
15300
|
+
*/
|
|
14058
15301
|
generateAndAlignTransitionFrames(from, to2, durationMs, useLinearInterpolation = false) {
|
|
14059
15302
|
const aligned = this.alignFlamePair(from, to2);
|
|
14060
15303
|
let keyframes = useLinearInterpolation ? generateTransitionFramesLinear(
|
|
@@ -14075,6 +15318,9 @@ class AvatarView {
|
|
|
14075
15318
|
keyframes[keyframes.length - 1] = aligned.to;
|
|
14076
15319
|
return keyframes;
|
|
14077
15320
|
}
|
|
15321
|
+
/**
|
|
15322
|
+
* 获取缓存的 Idle 首帧,如果未缓存则获取并缓存
|
|
15323
|
+
*/
|
|
14078
15324
|
async getCachedIdleFirstFrame() {
|
|
14079
15325
|
if (!this.cachedIdleFirstFrame) {
|
|
14080
15326
|
const avatarCore = AvatarSDK.getAvatarCore();
|
|
@@ -14089,9 +15335,15 @@ class AvatarView {
|
|
|
14089
15335
|
}
|
|
14090
15336
|
return this.cachedIdleFirstFrame;
|
|
14091
15337
|
}
|
|
15338
|
+
/**
|
|
15339
|
+
* Get controller (public interface)
|
|
15340
|
+
*/
|
|
14092
15341
|
get controller() {
|
|
14093
15342
|
return this.avatarController;
|
|
14094
15343
|
}
|
|
15344
|
+
/**
|
|
15345
|
+
* 创建canvas元素
|
|
15346
|
+
*/
|
|
14095
15347
|
createCanvas(container) {
|
|
14096
15348
|
const canvas = document.createElement("canvas");
|
|
14097
15349
|
const containerWidth = container.offsetWidth || 800;
|
|
@@ -14128,9 +15380,16 @@ class AvatarView {
|
|
|
14128
15380
|
window.addEventListener("resize", this.onWindowResize);
|
|
14129
15381
|
return canvas;
|
|
14130
15382
|
}
|
|
15383
|
+
/**
|
|
15384
|
+
* 获取canvas元素(供外部访问)
|
|
15385
|
+
* @internal
|
|
15386
|
+
*/
|
|
14131
15387
|
getCanvas() {
|
|
14132
15388
|
return this.canvas;
|
|
14133
15389
|
}
|
|
15390
|
+
/**
|
|
15391
|
+
* 初始化视图系统
|
|
15392
|
+
*/
|
|
14134
15393
|
async initializeView(avatar) {
|
|
14135
15394
|
var _a;
|
|
14136
15395
|
try {
|
|
@@ -14198,6 +15457,9 @@ class AvatarView {
|
|
|
14198
15457
|
throw error;
|
|
14199
15458
|
}
|
|
14200
15459
|
}
|
|
15460
|
+
/**
|
|
15461
|
+
* 初始化渲染系统
|
|
15462
|
+
*/
|
|
14201
15463
|
async initializeRenderSystem(cameraInfo) {
|
|
14202
15464
|
this.cameraConfig = cameraInfo || this.getDefaultCameraConfig();
|
|
14203
15465
|
if (cameraInfo) {
|
|
@@ -14209,15 +15471,23 @@ class AvatarView {
|
|
|
14209
15471
|
canvas: this.canvas,
|
|
14210
15472
|
camera: this.cameraConfig,
|
|
14211
15473
|
backgroundColor: [0, 0, 0, 0],
|
|
15474
|
+
// 透明背景
|
|
14212
15475
|
alpha: true
|
|
15476
|
+
// 启用 alpha 通道
|
|
14213
15477
|
});
|
|
14214
15478
|
await this.renderSystem.initialize();
|
|
14215
15479
|
if (APP_CONFIG.debug)
|
|
14216
15480
|
logger.log("[AvatarView] Render system initialized successfully");
|
|
14217
15481
|
}
|
|
15482
|
+
/**
|
|
15483
|
+
* 获取默认相机配置
|
|
15484
|
+
*/
|
|
14218
15485
|
getDefaultCameraConfig() {
|
|
14219
15486
|
return { ...APP_CONFIG.camera };
|
|
14220
15487
|
}
|
|
15488
|
+
/**
|
|
15489
|
+
* 根据资源解析最终的相机配置,优先使用角色设置,其次 camera.json
|
|
15490
|
+
*/
|
|
14221
15491
|
resolveCameraConfig(resources) {
|
|
14222
15492
|
var _a, _b;
|
|
14223
15493
|
const defaultCamera = this.getDefaultCameraConfig();
|
|
@@ -14231,6 +15501,9 @@ class AvatarView {
|
|
|
14231
15501
|
const source = characterCameraSettings ? "characterSettings" : "camera.json";
|
|
14232
15502
|
return this.deriveCameraConfigFromSettings(candidateSettings, defaultCamera, source);
|
|
14233
15503
|
}
|
|
15504
|
+
/**
|
|
15505
|
+
* 从角色设置中推导相机配置
|
|
15506
|
+
*/
|
|
14234
15507
|
deriveCameraConfigFromSettings(cameraSettings, fallback, source) {
|
|
14235
15508
|
const safeValue = (value, fallbackValue) => Number.isFinite(value) ? value : fallbackValue;
|
|
14236
15509
|
const translationX = safeValue(cameraSettings.translationX, fallback.position[0]);
|
|
@@ -14268,10 +15541,12 @@ class AvatarView {
|
|
|
14268
15541
|
const fov = hasCustomFov ? fovRadians * 180 / Math.PI : fallback.fov;
|
|
14269
15542
|
const derivedCamera = {
|
|
14270
15543
|
...fallback,
|
|
15544
|
+
// 自动继承 near/far 等配置(来自 APP_CONFIG.camera)
|
|
14271
15545
|
position,
|
|
14272
15546
|
target,
|
|
14273
15547
|
up,
|
|
14274
15548
|
fov
|
|
15549
|
+
// near/far 从 fallback 继承,无需硬编码
|
|
14275
15550
|
};
|
|
14276
15551
|
logger.log("[AvatarView] Applied camera settings from resources", {
|
|
14277
15552
|
source,
|
|
@@ -14285,6 +15560,9 @@ class AvatarView {
|
|
|
14285
15560
|
});
|
|
14286
15561
|
return derivedCamera;
|
|
14287
15562
|
}
|
|
15563
|
+
/**
|
|
15564
|
+
* 渲染第一帧
|
|
15565
|
+
*/
|
|
14288
15566
|
async renderFirstFrame() {
|
|
14289
15567
|
var _a;
|
|
14290
15568
|
if (!this.renderSystem) {
|
|
@@ -14316,6 +15594,9 @@ class AvatarView {
|
|
|
14316
15594
|
throw new Error("Failed to compute first frame splat data");
|
|
14317
15595
|
}
|
|
14318
15596
|
}
|
|
15597
|
+
/**
|
|
15598
|
+
* 更新 FPS 统计(在 requestAnimationFrame 回调中调用)
|
|
15599
|
+
*/
|
|
14319
15600
|
updateFPS() {
|
|
14320
15601
|
this.frameCount++;
|
|
14321
15602
|
const now = performance.now();
|
|
@@ -14325,10 +15606,16 @@ class AvatarView {
|
|
|
14325
15606
|
this.lastFpsUpdate = now;
|
|
14326
15607
|
}
|
|
14327
15608
|
}
|
|
15609
|
+
/**
|
|
15610
|
+
* 初始化 FPS 计算
|
|
15611
|
+
*/
|
|
14328
15612
|
initFPS() {
|
|
14329
15613
|
this.frameCount = 0;
|
|
14330
15614
|
this.lastFpsUpdate = performance.now();
|
|
14331
15615
|
}
|
|
15616
|
+
/**
|
|
15617
|
+
* 开始idle动画循环
|
|
15618
|
+
*/
|
|
14332
15619
|
startIdleAnimationLoop() {
|
|
14333
15620
|
if (this.idleAnimationLoopId) {
|
|
14334
15621
|
this.stopIdleAnimationLoop();
|
|
@@ -14388,6 +15675,9 @@ class AvatarView {
|
|
|
14388
15675
|
if (APP_CONFIG.debug)
|
|
14389
15676
|
logger.log("[AvatarView] Idle animation loop started");
|
|
14390
15677
|
}
|
|
15678
|
+
/**
|
|
15679
|
+
* 开始实时对话动画循环
|
|
15680
|
+
*/
|
|
14391
15681
|
startRealtimeAnimationLoop() {
|
|
14392
15682
|
if (this.realtimeAnimationLoopId) {
|
|
14393
15683
|
this.stopRealtimeAnimationLoop();
|
|
@@ -14412,10 +15702,16 @@ class AvatarView {
|
|
|
14412
15702
|
if (state === "transitioningToSpeaking" || state === "transitioningToIdle") {
|
|
14413
15703
|
if (this.transitionKeyframes.length === 0) {
|
|
14414
15704
|
if (state === "transitioningToSpeaking") {
|
|
14415
|
-
this.setState(
|
|
15705
|
+
this.setState(
|
|
15706
|
+
"speaking"
|
|
15707
|
+
/* Speaking */
|
|
15708
|
+
);
|
|
14416
15709
|
this.avatarController.onTransitionComplete();
|
|
14417
15710
|
} else if (state === "transitioningToIdle") {
|
|
14418
|
-
this.setState(
|
|
15711
|
+
this.setState(
|
|
15712
|
+
"idle"
|
|
15713
|
+
/* Idle */
|
|
15714
|
+
);
|
|
14419
15715
|
this.stopRealtimeAnimationLoop();
|
|
14420
15716
|
this.startIdleAnimationLoop();
|
|
14421
15717
|
return;
|
|
@@ -14441,11 +15737,17 @@ class AvatarView {
|
|
|
14441
15737
|
}
|
|
14442
15738
|
if (progress >= 1) {
|
|
14443
15739
|
if (state === "transitioningToSpeaking") {
|
|
14444
|
-
this.setState(
|
|
15740
|
+
this.setState(
|
|
15741
|
+
"speaking"
|
|
15742
|
+
/* Speaking */
|
|
15743
|
+
);
|
|
14445
15744
|
this.transitionKeyframes = [];
|
|
14446
15745
|
this.avatarController.onTransitionComplete();
|
|
14447
15746
|
} else if (state === "transitioningToIdle") {
|
|
14448
|
-
this.setState(
|
|
15747
|
+
this.setState(
|
|
15748
|
+
"idle"
|
|
15749
|
+
/* Idle */
|
|
15750
|
+
);
|
|
14449
15751
|
this.transitionKeyframes = [];
|
|
14450
15752
|
this.stopRealtimeAnimationLoop();
|
|
14451
15753
|
this.startIdleAnimationLoop();
|
|
@@ -14453,7 +15755,10 @@ class AvatarView {
|
|
|
14453
15755
|
}
|
|
14454
15756
|
}
|
|
14455
15757
|
if (state === "transitioningToSpeaking" && this.transitionStartTime > 0 && this.transitionKeyframes.length > 0 && elapsed >= this.startTransitionDurationMs + 100) {
|
|
14456
|
-
this.setState(
|
|
15758
|
+
this.setState(
|
|
15759
|
+
"speaking"
|
|
15760
|
+
/* Speaking */
|
|
15761
|
+
);
|
|
14457
15762
|
this.transitionKeyframes = [];
|
|
14458
15763
|
this.avatarController.onTransitionComplete();
|
|
14459
15764
|
}
|
|
@@ -14473,6 +15778,9 @@ class AvatarView {
|
|
|
14473
15778
|
if (APP_CONFIG.debug)
|
|
14474
15779
|
logger.log("[AvatarView] Realtime animation loop started");
|
|
14475
15780
|
}
|
|
15781
|
+
/**
|
|
15782
|
+
* 停止idle动画循环
|
|
15783
|
+
*/
|
|
14476
15784
|
stopIdleAnimationLoop() {
|
|
14477
15785
|
if (this.idleAnimationLoopId) {
|
|
14478
15786
|
cancelAnimationFrame(this.idleAnimationLoopId);
|
|
@@ -14481,6 +15789,9 @@ class AvatarView {
|
|
|
14481
15789
|
logger.log("[AvatarView] Idle animation loop stopped");
|
|
14482
15790
|
}
|
|
14483
15791
|
}
|
|
15792
|
+
/**
|
|
15793
|
+
* 停止实时对话动画循环
|
|
15794
|
+
*/
|
|
14484
15795
|
stopRealtimeAnimationLoop() {
|
|
14485
15796
|
if (this.realtimeAnimationLoopId) {
|
|
14486
15797
|
cancelAnimationFrame(this.realtimeAnimationLoopId);
|
|
@@ -14489,10 +15800,16 @@ class AvatarView {
|
|
|
14489
15800
|
logger.log("[AvatarView] Realtime animation loop stopped");
|
|
14490
15801
|
}
|
|
14491
15802
|
}
|
|
15803
|
+
/**
|
|
15804
|
+
* 停止所有动画循环
|
|
15805
|
+
*/
|
|
14492
15806
|
stopAllAnimationLoops() {
|
|
14493
15807
|
this.stopIdleAnimationLoop();
|
|
14494
15808
|
this.stopRealtimeAnimationLoop();
|
|
14495
15809
|
}
|
|
15810
|
+
/**
|
|
15811
|
+
* 渲染实时帧(由播放层回调调用)
|
|
15812
|
+
*/
|
|
14496
15813
|
renderRealtimeFrame(splatData, frameIndex) {
|
|
14497
15814
|
if (!this.renderSystem || this.renderingState !== "speaking") {
|
|
14498
15815
|
return;
|
|
@@ -14505,6 +15822,10 @@ class AvatarView {
|
|
|
14505
15822
|
this.currentPlayingFrame = this.lastRealtimeProtoFrame;
|
|
14506
15823
|
}
|
|
14507
15824
|
}
|
|
15825
|
+
/**
|
|
15826
|
+
* 状态转换方法
|
|
15827
|
+
* 统一管理状态转换,确保状态一致性
|
|
15828
|
+
*/
|
|
14508
15829
|
setState(newState) {
|
|
14509
15830
|
const oldState = this.renderingState;
|
|
14510
15831
|
this.renderingState = newState;
|
|
@@ -14523,15 +15844,28 @@ class AvatarView {
|
|
|
14523
15844
|
this.currentPlayingFrame = null;
|
|
14524
15845
|
}
|
|
14525
15846
|
}
|
|
15847
|
+
/**
|
|
15848
|
+
* 检查是否在实时播放状态(Speaking 或过渡到 Speaking)
|
|
15849
|
+
*/
|
|
14526
15850
|
get isRealtimePlaying() {
|
|
14527
15851
|
return this.renderingState === "speaking" || this.renderingState === "transitioningToSpeaking";
|
|
14528
15852
|
}
|
|
15853
|
+
/**
|
|
15854
|
+
* 检查是否在过渡中
|
|
15855
|
+
*/
|
|
14529
15856
|
get isTransitioning() {
|
|
14530
15857
|
return this.renderingState === "transitioningToSpeaking" || this.renderingState === "transitioningToIdle";
|
|
14531
15858
|
}
|
|
15859
|
+
/**
|
|
15860
|
+
* 检查过渡结束后是否回到 Idle
|
|
15861
|
+
*/
|
|
14532
15862
|
get endToIdleAfterTransition() {
|
|
14533
15863
|
return this.renderingState === "transitioningToIdle";
|
|
14534
15864
|
}
|
|
15865
|
+
/**
|
|
15866
|
+
* 处理打断
|
|
15867
|
+
* 打断时应该生成过渡动画,而不是直接跳回 Idle
|
|
15868
|
+
*/
|
|
14535
15869
|
handleInterrupt() {
|
|
14536
15870
|
const state = this.renderingState;
|
|
14537
15871
|
if (state === "idle") {
|
|
@@ -14543,11 +15877,17 @@ class AvatarView {
|
|
|
14543
15877
|
if (state === "speaking" || state === "transitioningToSpeaking") {
|
|
14544
15878
|
this.stopRealtimeRendering();
|
|
14545
15879
|
} else {
|
|
14546
|
-
this.setState(
|
|
15880
|
+
this.setState(
|
|
15881
|
+
"idle"
|
|
15882
|
+
/* Idle */
|
|
15883
|
+
);
|
|
14547
15884
|
this.stopRealtimeAnimationLoop();
|
|
14548
15885
|
this.startIdleAnimationLoop();
|
|
14549
15886
|
}
|
|
14550
15887
|
}
|
|
15888
|
+
/**
|
|
15889
|
+
* 设置 AvatarController 事件监听器
|
|
15890
|
+
*/
|
|
14551
15891
|
setupControllerEventListeners() {
|
|
14552
15892
|
this.avatarController.setupInternalEventListeners({
|
|
14553
15893
|
onKeyframesUpdate: (keyframes) => {
|
|
@@ -14564,6 +15904,10 @@ class AvatarView {
|
|
|
14564
15904
|
}
|
|
14565
15905
|
});
|
|
14566
15906
|
}
|
|
15907
|
+
/**
|
|
15908
|
+
* 准备实时渲染(生成过渡到 Speaking)
|
|
15909
|
+
* 统一逻辑:从当前正在播放的帧 -> Speaking 第一帧
|
|
15910
|
+
*/
|
|
14567
15911
|
async prepareRealtimeRendering(keyframes) {
|
|
14568
15912
|
const state = this.renderingState;
|
|
14569
15913
|
if ((state === "speaking" || state === "transitioningToSpeaking") && this.currentKeyframes.length > 0) {
|
|
@@ -14572,7 +15916,10 @@ class AvatarView {
|
|
|
14572
15916
|
}
|
|
14573
15917
|
this.stopIdleAnimationLoop();
|
|
14574
15918
|
this.currentKeyframes = keyframes;
|
|
14575
|
-
this.setState(
|
|
15919
|
+
this.setState(
|
|
15920
|
+
"transitioningToSpeaking"
|
|
15921
|
+
/* TransitioningToSpeaking */
|
|
15922
|
+
);
|
|
14576
15923
|
try {
|
|
14577
15924
|
const avatarCore = AvatarSDK.getAvatarCore();
|
|
14578
15925
|
if (avatarCore && keyframes.length > 0) {
|
|
@@ -14607,7 +15954,10 @@ class AvatarView {
|
|
|
14607
15954
|
this.transitionStartTime = performance.now();
|
|
14608
15955
|
this.currentPlayingFrame = null;
|
|
14609
15956
|
if (this.transitionKeyframes.length === 0) {
|
|
14610
|
-
this.setState(
|
|
15957
|
+
this.setState(
|
|
15958
|
+
"speaking"
|
|
15959
|
+
/* Speaking */
|
|
15960
|
+
);
|
|
14611
15961
|
this.avatarController.onTransitionComplete();
|
|
14612
15962
|
} else {
|
|
14613
15963
|
if (APP_CONFIG.debug)
|
|
@@ -14617,12 +15967,18 @@ class AvatarView {
|
|
|
14617
15967
|
} catch (e2) {
|
|
14618
15968
|
logger.warn("[AvatarView] Transition generation failed:", e2 instanceof Error ? e2.message : String(e2));
|
|
14619
15969
|
if (this.renderingState === "transitioningToSpeaking") {
|
|
14620
|
-
this.setState(
|
|
15970
|
+
this.setState(
|
|
15971
|
+
"speaking"
|
|
15972
|
+
/* Speaking */
|
|
15973
|
+
);
|
|
14621
15974
|
this.avatarController.onTransitionComplete();
|
|
14622
15975
|
}
|
|
14623
15976
|
}
|
|
14624
15977
|
this.startRealtimeAnimationLoop();
|
|
14625
15978
|
}
|
|
15979
|
+
/**
|
|
15980
|
+
* 开始实时渲染循环
|
|
15981
|
+
*/
|
|
14626
15982
|
startRealtimeRendering() {
|
|
14627
15983
|
if (APP_CONFIG.debug)
|
|
14628
15984
|
logger.log("[AvatarView] Starting realtime rendering with", this.currentKeyframes.length, "frames");
|
|
@@ -14632,6 +15988,9 @@ class AvatarView {
|
|
|
14632
15988
|
keyframesCount: this.currentKeyframes.length
|
|
14633
15989
|
});
|
|
14634
15990
|
}
|
|
15991
|
+
/**
|
|
15992
|
+
* 停止实时对话渲染
|
|
15993
|
+
*/
|
|
14635
15994
|
stopRealtimeRendering() {
|
|
14636
15995
|
var _a, _b;
|
|
14637
15996
|
const state = this.renderingState;
|
|
@@ -14642,12 +16001,18 @@ class AvatarView {
|
|
|
14642
16001
|
if (state === "transitioningToIdle") {
|
|
14643
16002
|
return;
|
|
14644
16003
|
}
|
|
14645
|
-
this.setState(
|
|
16004
|
+
this.setState(
|
|
16005
|
+
"idle"
|
|
16006
|
+
/* Idle */
|
|
16007
|
+
);
|
|
14646
16008
|
this.stopRealtimeAnimationLoop();
|
|
14647
16009
|
this.startIdleAnimationLoop();
|
|
14648
16010
|
return;
|
|
14649
16011
|
}
|
|
14650
|
-
this.setState(
|
|
16012
|
+
this.setState(
|
|
16013
|
+
"transitioningToIdle"
|
|
16014
|
+
/* TransitioningToIdle */
|
|
16015
|
+
);
|
|
14651
16016
|
(_b = (_a = this.avatarController).onConversationState) == null ? void 0 : _b.call(_a, ConversationState.idle);
|
|
14652
16017
|
(async () => {
|
|
14653
16018
|
try {
|
|
@@ -14673,7 +16038,10 @@ class AvatarView {
|
|
|
14673
16038
|
}
|
|
14674
16039
|
if (!fromFrame) {
|
|
14675
16040
|
logger.warn("[AvatarView] Cannot get current playing frame for transition to idle, fallback to idle frame");
|
|
14676
|
-
this.setState(
|
|
16041
|
+
this.setState(
|
|
16042
|
+
"idle"
|
|
16043
|
+
/* Idle */
|
|
16044
|
+
);
|
|
14677
16045
|
this.stopRealtimeAnimationLoop();
|
|
14678
16046
|
this.startIdleAnimationLoop();
|
|
14679
16047
|
return;
|
|
@@ -14698,12 +16066,19 @@ class AvatarView {
|
|
|
14698
16066
|
logger.warn("[AvatarView] Return transition generation failed:", e2 instanceof Error ? e2.message : String(e2));
|
|
14699
16067
|
}
|
|
14700
16068
|
if (this.renderingState === "transitioningToIdle") {
|
|
14701
|
-
this.setState(
|
|
16069
|
+
this.setState(
|
|
16070
|
+
"idle"
|
|
16071
|
+
/* Idle */
|
|
16072
|
+
);
|
|
14702
16073
|
this.stopRealtimeAnimationLoop();
|
|
14703
16074
|
this.startIdleAnimationLoop();
|
|
14704
16075
|
}
|
|
14705
16076
|
})();
|
|
14706
16077
|
}
|
|
16078
|
+
/**
|
|
16079
|
+
* Cleanup view resources
|
|
16080
|
+
* Closes avatarController and cleans up all related resources
|
|
16081
|
+
*/
|
|
14707
16082
|
dispose() {
|
|
14708
16083
|
if (APP_CONFIG.debug)
|
|
14709
16084
|
logger.log("[AvatarView] Disposing avatar view...");
|
|
@@ -14717,7 +16092,10 @@ class AvatarView {
|
|
|
14717
16092
|
}
|
|
14718
16093
|
this.stopAllAnimationLoops();
|
|
14719
16094
|
this.stopAvatarActiveHeartbeat();
|
|
14720
|
-
this.setState(
|
|
16095
|
+
this.setState(
|
|
16096
|
+
"idle"
|
|
16097
|
+
/* Idle */
|
|
16098
|
+
);
|
|
14721
16099
|
this.cachedIdleFirstFrame = null;
|
|
14722
16100
|
this.idleCurrentFrameIndex = 0;
|
|
14723
16101
|
this.currentPlayingFrame = null;
|
|
@@ -14748,9 +16126,17 @@ class AvatarView {
|
|
|
14748
16126
|
if (APP_CONFIG.debug)
|
|
14749
16127
|
logger.log("[AvatarView] Avatar view disposed successfully");
|
|
14750
16128
|
}
|
|
16129
|
+
/**
|
|
16130
|
+
* 获取相机配置
|
|
16131
|
+
* @internal
|
|
16132
|
+
*/
|
|
14751
16133
|
getCameraConfig() {
|
|
14752
16134
|
return this.cameraConfig;
|
|
14753
16135
|
}
|
|
16136
|
+
/**
|
|
16137
|
+
* 更新相机配置
|
|
16138
|
+
* @internal
|
|
16139
|
+
*/
|
|
14754
16140
|
updateCameraConfig(cameraConfig) {
|
|
14755
16141
|
this.cameraConfig = cameraConfig;
|
|
14756
16142
|
if (APP_CONFIG.debug)
|
|
@@ -14764,7 +16150,13 @@ class AvatarView {
|
|
|
14764
16150
|
}
|
|
14765
16151
|
}
|
|
14766
16152
|
}
|
|
14767
|
-
|
|
16153
|
+
/**
|
|
16154
|
+
* Render specified keyframe (pure rendering mode, no audio-animation synchronization)
|
|
16155
|
+
* Suitable for scenarios where external application controls audio playback and animation playback
|
|
16156
|
+
* @param keyframeData - Keyframe data
|
|
16157
|
+
* @param enableIdleRendering - Whether to enable idle loop rendering. true: Enable idle rendering and return immediately (skip this keyframe); false: Disable idle rendering and process keyframe (default)
|
|
16158
|
+
*/
|
|
16159
|
+
async renderFlame(keyframeData, enableIdleRendering) {
|
|
14768
16160
|
if (!this.isInitialized || !this.renderSystem) {
|
|
14769
16161
|
throw new Error("AvatarView not initialized");
|
|
14770
16162
|
}
|
|
@@ -14774,6 +16166,7 @@ class AvatarView {
|
|
|
14774
16166
|
}
|
|
14775
16167
|
this.isPureRenderingMode = true;
|
|
14776
16168
|
try {
|
|
16169
|
+
const flame = keyframeData;
|
|
14777
16170
|
const processedFlame = this.avatarController.applyPostProcessingToFlame(flame);
|
|
14778
16171
|
const wasmParams = convertProtoFlameToWasmParams(processedFlame);
|
|
14779
16172
|
const avatarCore = AvatarSDK.getAvatarCore();
|
|
@@ -14793,7 +16186,14 @@ class AvatarView {
|
|
|
14793
16186
|
throw error;
|
|
14794
16187
|
}
|
|
14795
16188
|
}
|
|
14796
|
-
|
|
16189
|
+
/**
|
|
16190
|
+
* Generate transition frame array from current idle frame to target keyframe (pure rendering mode)
|
|
16191
|
+
* @param toKeyframeData - Target keyframe data
|
|
16192
|
+
* @param frameCount - Number of transition frames
|
|
16193
|
+
* @param transitionType - Transition type: 'start' means Idle -> toKeyframeData (start transition), 'end' means toKeyframeData -> Idle (end transition)
|
|
16194
|
+
* @returns Transition frame array with length of frameCount
|
|
16195
|
+
*/
|
|
16196
|
+
async generateTransitionFromIdle(toKeyframeData, frameCount, transitionType = "start") {
|
|
14797
16197
|
if (!this.isInitialized) {
|
|
14798
16198
|
throw new Error("AvatarView not initialized");
|
|
14799
16199
|
}
|
|
@@ -14807,6 +16207,7 @@ class AvatarView {
|
|
|
14807
16207
|
try {
|
|
14808
16208
|
const idleParams = await avatarCore.getCurrentFrameParams(this.idleCurrentFrameIndex, this.characterId);
|
|
14809
16209
|
const idleFrameProto = convertWasmParamsToProtoFlame(idleParams);
|
|
16210
|
+
const toFlame = toKeyframeData;
|
|
14810
16211
|
const toFlameWithPostProcessing = this.avatarController.applyPostProcessingToFlame(toFlame);
|
|
14811
16212
|
const aligned = this.alignFlamePair(idleFrameProto, toFlameWithPostProcessing);
|
|
14812
16213
|
const from = transitionType === "start" ? aligned.from : aligned.to;
|
|
@@ -14833,17 +16234,31 @@ class AvatarView {
|
|
|
14833
16234
|
throw error;
|
|
14834
16235
|
}
|
|
14835
16236
|
}
|
|
16237
|
+
/**
|
|
16238
|
+
* 使用新的相机配置重新渲染当前帧(用于暂停状态下更新相机)
|
|
16239
|
+
* 复用 AvatarController 的重新渲染逻辑,因为 renderCallback 会调用 renderRealtimeFrame,
|
|
16240
|
+
* 而 renderRealtimeFrame 会使用已更新的相机配置(通过 renderSystem.updateCamera)
|
|
16241
|
+
* @private
|
|
16242
|
+
*/
|
|
14836
16243
|
async rerenderCurrentFrameWithNewCamera() {
|
|
14837
16244
|
if (this.avatarController.state !== AvatarState.paused || this.renderingState !== "speaking" || !this.renderSystem) {
|
|
14838
16245
|
return;
|
|
14839
16246
|
}
|
|
14840
16247
|
await this.avatarController.rerenderCurrentFrameIfPaused();
|
|
14841
16248
|
}
|
|
16249
|
+
/**
|
|
16250
|
+
* 处理尺寸变化:通知渲染系统更新视口与投影
|
|
16251
|
+
*/
|
|
14842
16252
|
handleResize() {
|
|
14843
16253
|
if (this.renderSystem) {
|
|
14844
16254
|
this.renderSystem.handleResize();
|
|
14845
16255
|
}
|
|
14846
16256
|
}
|
|
16257
|
+
/**
|
|
16258
|
+
* 获取渲染性能统计
|
|
16259
|
+
* @returns 渲染性能统计数据,如果渲染系统未初始化则返回 null
|
|
16260
|
+
* @internal Not part of public API yet
|
|
16261
|
+
*/
|
|
14847
16262
|
getPerformanceStats() {
|
|
14848
16263
|
if (!this.renderSystem || !this.isInitialized) {
|
|
14849
16264
|
return null;
|
|
@@ -14853,8 +16268,19 @@ class AvatarView {
|
|
|
14853
16268
|
sortTime: this.renderSystem.sortTime,
|
|
14854
16269
|
backend: this.renderSystem.getBackend(),
|
|
14855
16270
|
fps: this.currentFPS
|
|
16271
|
+
// pointCount 可后续通过 AvatarCoreAdapter 添加公开方法获取
|
|
14856
16272
|
};
|
|
14857
16273
|
}
|
|
16274
|
+
/**
|
|
16275
|
+
* Get or set avatar transform in canvas
|
|
16276
|
+
*
|
|
16277
|
+
* @example
|
|
16278
|
+
* // Get current transform
|
|
16279
|
+
* const current = avatarView.transform
|
|
16280
|
+
*
|
|
16281
|
+
* // Set transform
|
|
16282
|
+
* avatarView.transform = { x: 0.5, y: 0, scale: 2.0 }
|
|
16283
|
+
*/
|
|
14858
16284
|
get transform() {
|
|
14859
16285
|
if (!this.renderSystem) {
|
|
14860
16286
|
throw new Error("Render system not initialized");
|
|
@@ -14872,6 +16298,10 @@ class AvatarView {
|
|
|
14872
16298
|
this.renderSystem.renderFrame();
|
|
14873
16299
|
}
|
|
14874
16300
|
}
|
|
16301
|
+
/**
|
|
16302
|
+
* 上报 avatar_active 埋点
|
|
16303
|
+
* @private
|
|
16304
|
+
*/
|
|
14875
16305
|
reportAvatarActive() {
|
|
14876
16306
|
var _a, _b;
|
|
14877
16307
|
logEvent("avatar_active", "info", {
|
|
@@ -14880,6 +16310,10 @@ class AvatarView {
|
|
|
14880
16310
|
dsm: ((_b = AvatarSDK.configuration) == null ? void 0 : _b.drivingServiceMode) || DrivingServiceMode.sdk
|
|
14881
16311
|
});
|
|
14882
16312
|
}
|
|
16313
|
+
/**
|
|
16314
|
+
* 启动 avatar_active 心跳埋点(每10分钟一次)
|
|
16315
|
+
* @private
|
|
16316
|
+
*/
|
|
14883
16317
|
startAvatarActiveHeartbeat() {
|
|
14884
16318
|
this.stopAvatarActiveHeartbeat();
|
|
14885
16319
|
this.avatarActiveTimer = window.setInterval(() => {
|
|
@@ -14888,6 +16322,10 @@ class AvatarView {
|
|
|
14888
16322
|
}
|
|
14889
16323
|
}, this.AVATAR_ACTIVE_INTERVAL);
|
|
14890
16324
|
}
|
|
16325
|
+
/**
|
|
16326
|
+
* 停止 avatar_active 心跳埋点
|
|
16327
|
+
* @private
|
|
16328
|
+
*/
|
|
14891
16329
|
stopAvatarActiveHeartbeat() {
|
|
14892
16330
|
if (this.avatarActiveTimer !== null) {
|
|
14893
16331
|
clearInterval(this.avatarActiveTimer);
|