@spatius/avatarkit 1.1.0-beta.1 → 1.2.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/{StreamingAudioPlayer-DrHXn-YA.js → StreamingAudioPlayer-Br8UrVgO.js} +1 -1
- package/dist/core/AvatarController.d.ts +28 -1
- package/dist/{index-CVQn5uEB.js → index-DHCQ9wsL.js} +391 -23
- package/dist/index.js +2 -1
- package/dist/types/index.d.ts +13 -0
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.2.0-beta.1] - 2026-06-19
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Configurable frame-starvation handling. New public API:
|
|
12
|
+
- `FrameStarvationMode` enum with `audioIndependent` (default) and `strictSync`.
|
|
13
|
+
- `AvatarController.frameStarvationMode: FrameStarvationMode` — choose how playback behaves when animation frames can't keep up with audio. `audioIndependent` keeps audio playing while animation catches up (previous behavior); `strictSync` pauses audio until frames arrive, keeping audio and animation strictly in sync.
|
|
14
|
+
- `AvatarController.onPlaybackStall: ((stalled: boolean) => void) | null` — fires when audio is paused/resumed due to frame starvation (only in `strictSync`).
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- Avatars now load via the latest avatar asset format.
|
|
18
|
+
- CDN selection now adapts to the configured region.
|
|
19
|
+
|
|
8
20
|
## [1.1.0-beta.1] - 2026-06-08
|
|
9
21
|
|
|
10
22
|
First 1.1 pre-release. Includes breaking changes — see below.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
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
|
-
import { A as APP_CONFIG, l as logger, e as errorToMessage, a as logEvent } from "./index-
|
|
4
|
+
import { A as APP_CONFIG, l as logger, e as errorToMessage, a as logEvent } from "./index-DHCQ9wsL.js";
|
|
5
5
|
class StreamingAudioPlayer {
|
|
6
6
|
// Mark if AudioContext is being resumed, avoid concurrent resume requests
|
|
7
7
|
constructor(options) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Avatar } from './Avatar';
|
|
2
|
-
import { ConnectionState, AvatarError, DrivingServiceMode, ConversationState, AnimationType, PostProcessingConfig } from '../types';
|
|
2
|
+
import { ConnectionState, AvatarError, DrivingServiceMode, FrameStarvationMode, ConversationState, AnimationType, PostProcessingConfig } from '../types';
|
|
3
3
|
import { FrameRateInfo } from '../performance/FrameRateMonitor';
|
|
4
4
|
export declare class AvatarController {
|
|
5
5
|
private networkLayer?;
|
|
@@ -12,6 +12,21 @@ export declare class AvatarController {
|
|
|
12
12
|
onError: ((error: AvatarError) => void) | null;
|
|
13
13
|
/** Callback for animation type changes (e.g., idle → mono in fallback mode). Aligned with iOS/Android AvatarController.onAnimationState. */
|
|
14
14
|
onAnimationState: ((type: AnimationType) => void) | null;
|
|
15
|
+
/**
|
|
16
|
+
* Strategy for handling animation-frame starvation. Default is
|
|
17
|
+
* {@link FrameStarvationMode.audioIndependent} (audio keeps playing, starvation only
|
|
18
|
+
* reported as telemetry — historical behavior). Set to {@link FrameStarvationMode.strictSync}
|
|
19
|
+
* to pause audio when frames run out and resume on new frames, notified via {@link onPlaybackStall}.
|
|
20
|
+
* Aligned with iOS/Android AvatarController.frameStarvationMode.
|
|
21
|
+
*/
|
|
22
|
+
frameStarvationMode: FrameStarvationMode;
|
|
23
|
+
/**
|
|
24
|
+
* Fires when audio is paused/resumed due to frame starvation. Only invoked in
|
|
25
|
+
* {@link FrameStarvationMode.strictSync}. `stalled=true` — frames ran out, audio paused;
|
|
26
|
+
* `stalled=false` — new frames arrived, audio resumed (or conversation ended / fell back).
|
|
27
|
+
* Aligned with iOS/Android AvatarController.onPlaybackStall.
|
|
28
|
+
*/
|
|
29
|
+
onPlaybackStall: ((stalled: boolean) => void) | null;
|
|
15
30
|
private eventListeners;
|
|
16
31
|
private readonly frameRateMonitor;
|
|
17
32
|
/** Frame rate monitoring callback. Fires with aggregated metrics from a 2-second sliding window. */
|
|
@@ -32,6 +47,18 @@ export declare class AvatarController {
|
|
|
32
47
|
private isFallbackMode;
|
|
33
48
|
private frameStarvationEvents;
|
|
34
49
|
private isFrameStarved;
|
|
50
|
+
/**
|
|
51
|
+
* Whether this round's final animation batch (ServerResponseAnimation.end) has arrived.
|
|
52
|
+
* Frame starvation is only possible BEFORE this — once all frames are in, any remaining
|
|
53
|
+
* audio tail is normal end-of-round, not starvation, so audio must keep playing to idle
|
|
54
|
+
* and must never be paused (matters most in strictSync). Reset per conversation.
|
|
55
|
+
*/
|
|
56
|
+
private animationEnded;
|
|
57
|
+
/**
|
|
58
|
+
* Whether audio is currently paused because of frame starvation (strictSync only).
|
|
59
|
+
* Orthogonal to user pause — user pause/resume does not change it; only frame arrival does.
|
|
60
|
+
*/
|
|
61
|
+
private isAudioStalledForStarvation;
|
|
35
62
|
private playbackStuckCheckState;
|
|
36
63
|
private readonly MAX_AUDIO_TIME_ZERO_COUNT;
|
|
37
64
|
private readonly MAX_AUDIO_TIME_STUCK_COUNT;
|
|
@@ -1257,6 +1257,7 @@ function messageTypeToJSON(object) {
|
|
|
1257
1257
|
}
|
|
1258
1258
|
var AudioFormat = /* @__PURE__ */ ((AudioFormat2) => {
|
|
1259
1259
|
AudioFormat2[AudioFormat2["AUDIO_FORMAT_PCM_S16LE"] = 0] = "AUDIO_FORMAT_PCM_S16LE";
|
|
1260
|
+
AudioFormat2[AudioFormat2["AUDIO_FORMAT_OGG_OPUS"] = 1] = "AUDIO_FORMAT_OGG_OPUS";
|
|
1260
1261
|
AudioFormat2[AudioFormat2["UNRECOGNIZED"] = -1] = "UNRECOGNIZED";
|
|
1261
1262
|
return AudioFormat2;
|
|
1262
1263
|
})(AudioFormat || {});
|
|
@@ -1265,6 +1266,9 @@ function audioFormatFromJSON(object) {
|
|
|
1265
1266
|
case 0:
|
|
1266
1267
|
case "AUDIO_FORMAT_PCM_S16LE":
|
|
1267
1268
|
return 0;
|
|
1269
|
+
case 1:
|
|
1270
|
+
case "AUDIO_FORMAT_OGG_OPUS":
|
|
1271
|
+
return 1;
|
|
1268
1272
|
case -1:
|
|
1269
1273
|
case "UNRECOGNIZED":
|
|
1270
1274
|
default:
|
|
@@ -1275,6 +1279,8 @@ function audioFormatToJSON(object) {
|
|
|
1275
1279
|
switch (object) {
|
|
1276
1280
|
case 0:
|
|
1277
1281
|
return "AUDIO_FORMAT_PCM_S16LE";
|
|
1282
|
+
case 1:
|
|
1283
|
+
return "AUDIO_FORMAT_OGG_OPUS";
|
|
1278
1284
|
case -1:
|
|
1279
1285
|
default:
|
|
1280
1286
|
return "UNRECOGNIZED";
|
|
@@ -1343,7 +1349,16 @@ function egressTypeToJSON(object) {
|
|
|
1343
1349
|
}
|
|
1344
1350
|
}
|
|
1345
1351
|
function createBaseLiveKitEgressConfig() {
|
|
1346
|
-
return {
|
|
1352
|
+
return {
|
|
1353
|
+
url: "",
|
|
1354
|
+
apiKey: "",
|
|
1355
|
+
apiSecret: "",
|
|
1356
|
+
roomName: "",
|
|
1357
|
+
publisherId: "",
|
|
1358
|
+
extraAttributes: {},
|
|
1359
|
+
idleTimeout: 0,
|
|
1360
|
+
apiToken: ""
|
|
1361
|
+
};
|
|
1347
1362
|
}
|
|
1348
1363
|
const LiveKitEgressConfig = {
|
|
1349
1364
|
encode(message, writer = new BinaryWriter()) {
|
|
@@ -1362,6 +1377,15 @@ const LiveKitEgressConfig = {
|
|
|
1362
1377
|
if (message.publisherId !== "") {
|
|
1363
1378
|
writer.uint32(42).string(message.publisherId);
|
|
1364
1379
|
}
|
|
1380
|
+
globalThis.Object.entries(message.extraAttributes).forEach(([key, value]) => {
|
|
1381
|
+
LiveKitEgressConfig_ExtraAttributesEntry.encode({ key, value }, writer.uint32(50).fork()).join();
|
|
1382
|
+
});
|
|
1383
|
+
if (message.idleTimeout !== 0) {
|
|
1384
|
+
writer.uint32(56).int32(message.idleTimeout);
|
|
1385
|
+
}
|
|
1386
|
+
if (message.apiToken !== "") {
|
|
1387
|
+
writer.uint32(66).string(message.apiToken);
|
|
1388
|
+
}
|
|
1365
1389
|
return writer;
|
|
1366
1390
|
},
|
|
1367
1391
|
decode(input, length) {
|
|
@@ -1406,6 +1430,30 @@ const LiveKitEgressConfig = {
|
|
|
1406
1430
|
message.publisherId = reader.string();
|
|
1407
1431
|
continue;
|
|
1408
1432
|
}
|
|
1433
|
+
case 6: {
|
|
1434
|
+
if (tag !== 50) {
|
|
1435
|
+
break;
|
|
1436
|
+
}
|
|
1437
|
+
const entry6 = LiveKitEgressConfig_ExtraAttributesEntry.decode(reader, reader.uint32());
|
|
1438
|
+
if (entry6.value !== void 0) {
|
|
1439
|
+
message.extraAttributes[entry6.key] = entry6.value;
|
|
1440
|
+
}
|
|
1441
|
+
continue;
|
|
1442
|
+
}
|
|
1443
|
+
case 7: {
|
|
1444
|
+
if (tag !== 56) {
|
|
1445
|
+
break;
|
|
1446
|
+
}
|
|
1447
|
+
message.idleTimeout = reader.int32();
|
|
1448
|
+
continue;
|
|
1449
|
+
}
|
|
1450
|
+
case 8: {
|
|
1451
|
+
if (tag !== 66) {
|
|
1452
|
+
break;
|
|
1453
|
+
}
|
|
1454
|
+
message.apiToken = reader.string();
|
|
1455
|
+
continue;
|
|
1456
|
+
}
|
|
1409
1457
|
}
|
|
1410
1458
|
if ((tag & 7) === 4 || tag === 0) {
|
|
1411
1459
|
break;
|
|
@@ -1420,7 +1468,22 @@ const LiveKitEgressConfig = {
|
|
|
1420
1468
|
apiKey: isSet(object.apiKey) ? globalThis.String(object.apiKey) : isSet(object.api_key) ? globalThis.String(object.api_key) : "",
|
|
1421
1469
|
apiSecret: isSet(object.apiSecret) ? globalThis.String(object.apiSecret) : isSet(object.api_secret) ? globalThis.String(object.api_secret) : "",
|
|
1422
1470
|
roomName: isSet(object.roomName) ? globalThis.String(object.roomName) : isSet(object.room_name) ? globalThis.String(object.room_name) : "",
|
|
1423
|
-
publisherId: isSet(object.publisherId) ? globalThis.String(object.publisherId) : isSet(object.publisher_id) ? globalThis.String(object.publisher_id) : ""
|
|
1471
|
+
publisherId: isSet(object.publisherId) ? globalThis.String(object.publisherId) : isSet(object.publisher_id) ? globalThis.String(object.publisher_id) : "",
|
|
1472
|
+
extraAttributes: isObject(object.extraAttributes) ? globalThis.Object.entries(object.extraAttributes).reduce(
|
|
1473
|
+
(acc, [key, value]) => {
|
|
1474
|
+
acc[key] = globalThis.String(value);
|
|
1475
|
+
return acc;
|
|
1476
|
+
},
|
|
1477
|
+
{}
|
|
1478
|
+
) : isObject(object.extra_attributes) ? globalThis.Object.entries(object.extra_attributes).reduce(
|
|
1479
|
+
(acc, [key, value]) => {
|
|
1480
|
+
acc[key] = globalThis.String(value);
|
|
1481
|
+
return acc;
|
|
1482
|
+
},
|
|
1483
|
+
{}
|
|
1484
|
+
) : {},
|
|
1485
|
+
idleTimeout: isSet(object.idleTimeout) ? globalThis.Number(object.idleTimeout) : isSet(object.idle_timeout) ? globalThis.Number(object.idle_timeout) : 0,
|
|
1486
|
+
apiToken: isSet(object.apiToken) ? globalThis.String(object.apiToken) : isSet(object.api_token) ? globalThis.String(object.api_token) : ""
|
|
1424
1487
|
};
|
|
1425
1488
|
},
|
|
1426
1489
|
toJSON(message) {
|
|
@@ -1440,6 +1503,21 @@ const LiveKitEgressConfig = {
|
|
|
1440
1503
|
if (message.publisherId !== "") {
|
|
1441
1504
|
obj.publisherId = message.publisherId;
|
|
1442
1505
|
}
|
|
1506
|
+
if (message.extraAttributes) {
|
|
1507
|
+
const entries = globalThis.Object.entries(message.extraAttributes);
|
|
1508
|
+
if (entries.length > 0) {
|
|
1509
|
+
obj.extraAttributes = {};
|
|
1510
|
+
entries.forEach(([k2, v2]) => {
|
|
1511
|
+
obj.extraAttributes[k2] = v2;
|
|
1512
|
+
});
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
if (message.idleTimeout !== 0) {
|
|
1516
|
+
obj.idleTimeout = Math.round(message.idleTimeout);
|
|
1517
|
+
}
|
|
1518
|
+
if (message.apiToken !== "") {
|
|
1519
|
+
obj.apiToken = message.apiToken;
|
|
1520
|
+
}
|
|
1443
1521
|
return obj;
|
|
1444
1522
|
},
|
|
1445
1523
|
create(base) {
|
|
@@ -1452,6 +1530,85 @@ const LiveKitEgressConfig = {
|
|
|
1452
1530
|
message.apiSecret = object.apiSecret ?? "";
|
|
1453
1531
|
message.roomName = object.roomName ?? "";
|
|
1454
1532
|
message.publisherId = object.publisherId ?? "";
|
|
1533
|
+
message.extraAttributes = globalThis.Object.entries(object.extraAttributes ?? {}).reduce(
|
|
1534
|
+
(acc, [key, value]) => {
|
|
1535
|
+
if (value !== void 0) {
|
|
1536
|
+
acc[key] = globalThis.String(value);
|
|
1537
|
+
}
|
|
1538
|
+
return acc;
|
|
1539
|
+
},
|
|
1540
|
+
{}
|
|
1541
|
+
);
|
|
1542
|
+
message.idleTimeout = object.idleTimeout ?? 0;
|
|
1543
|
+
message.apiToken = object.apiToken ?? "";
|
|
1544
|
+
return message;
|
|
1545
|
+
}
|
|
1546
|
+
};
|
|
1547
|
+
function createBaseLiveKitEgressConfig_ExtraAttributesEntry() {
|
|
1548
|
+
return { key: "", value: "" };
|
|
1549
|
+
}
|
|
1550
|
+
const LiveKitEgressConfig_ExtraAttributesEntry = {
|
|
1551
|
+
encode(message, writer = new BinaryWriter()) {
|
|
1552
|
+
if (message.key !== "") {
|
|
1553
|
+
writer.uint32(10).string(message.key);
|
|
1554
|
+
}
|
|
1555
|
+
if (message.value !== "") {
|
|
1556
|
+
writer.uint32(18).string(message.value);
|
|
1557
|
+
}
|
|
1558
|
+
return writer;
|
|
1559
|
+
},
|
|
1560
|
+
decode(input, length) {
|
|
1561
|
+
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
|
1562
|
+
const end = length === void 0 ? reader.len : reader.pos + length;
|
|
1563
|
+
const message = createBaseLiveKitEgressConfig_ExtraAttributesEntry();
|
|
1564
|
+
while (reader.pos < end) {
|
|
1565
|
+
const tag = reader.uint32();
|
|
1566
|
+
switch (tag >>> 3) {
|
|
1567
|
+
case 1: {
|
|
1568
|
+
if (tag !== 10) {
|
|
1569
|
+
break;
|
|
1570
|
+
}
|
|
1571
|
+
message.key = reader.string();
|
|
1572
|
+
continue;
|
|
1573
|
+
}
|
|
1574
|
+
case 2: {
|
|
1575
|
+
if (tag !== 18) {
|
|
1576
|
+
break;
|
|
1577
|
+
}
|
|
1578
|
+
message.value = reader.string();
|
|
1579
|
+
continue;
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
if ((tag & 7) === 4 || tag === 0) {
|
|
1583
|
+
break;
|
|
1584
|
+
}
|
|
1585
|
+
reader.skip(tag & 7);
|
|
1586
|
+
}
|
|
1587
|
+
return message;
|
|
1588
|
+
},
|
|
1589
|
+
fromJSON(object) {
|
|
1590
|
+
return {
|
|
1591
|
+
key: isSet(object.key) ? globalThis.String(object.key) : "",
|
|
1592
|
+
value: isSet(object.value) ? globalThis.String(object.value) : ""
|
|
1593
|
+
};
|
|
1594
|
+
},
|
|
1595
|
+
toJSON(message) {
|
|
1596
|
+
const obj = {};
|
|
1597
|
+
if (message.key !== "") {
|
|
1598
|
+
obj.key = message.key;
|
|
1599
|
+
}
|
|
1600
|
+
if (message.value !== "") {
|
|
1601
|
+
obj.value = message.value;
|
|
1602
|
+
}
|
|
1603
|
+
return obj;
|
|
1604
|
+
},
|
|
1605
|
+
create(base) {
|
|
1606
|
+
return LiveKitEgressConfig_ExtraAttributesEntry.fromPartial(base ?? {});
|
|
1607
|
+
},
|
|
1608
|
+
fromPartial(object) {
|
|
1609
|
+
const message = createBaseLiveKitEgressConfig_ExtraAttributesEntry();
|
|
1610
|
+
message.key = object.key ?? "";
|
|
1611
|
+
message.value = object.value ?? "";
|
|
1455
1612
|
return message;
|
|
1456
1613
|
}
|
|
1457
1614
|
};
|
|
@@ -3999,6 +4156,11 @@ var DrivingServiceMode = /* @__PURE__ */ ((DrivingServiceMode2) => {
|
|
|
3999
4156
|
DrivingServiceMode2["backend"] = "backend";
|
|
4000
4157
|
return DrivingServiceMode2;
|
|
4001
4158
|
})(DrivingServiceMode || {});
|
|
4159
|
+
var FrameStarvationMode = /* @__PURE__ */ ((FrameStarvationMode2) => {
|
|
4160
|
+
FrameStarvationMode2["audioIndependent"] = "audioIndependent";
|
|
4161
|
+
FrameStarvationMode2["strictSync"] = "strictSync";
|
|
4162
|
+
return FrameStarvationMode2;
|
|
4163
|
+
})(FrameStarvationMode || {});
|
|
4002
4164
|
var LogLevel = /* @__PURE__ */ ((LogLevel2) => {
|
|
4003
4165
|
LogLevel2["off"] = "off";
|
|
4004
4166
|
LogLevel2["error"] = "error";
|
|
@@ -12349,6 +12511,11 @@ function convertWasmParamsToProtoFlame(wasmParams) {
|
|
|
12349
12511
|
expression: wasmParams.expr_params || []
|
|
12350
12512
|
};
|
|
12351
12513
|
}
|
|
12514
|
+
const GLOBAL_FLAME_CDN_BASE = "https://cdn.spatialwalk.cloud/public";
|
|
12515
|
+
const CN_FLAME_CDN_BASE = "https://cdn.spatialwalk.top/public";
|
|
12516
|
+
function getFlameCdnBase(region) {
|
|
12517
|
+
return region.startsWith("cn-") ? CN_FLAME_CDN_BASE : GLOBAL_FLAME_CDN_BASE;
|
|
12518
|
+
}
|
|
12352
12519
|
const APP_CONFIG = {
|
|
12353
12520
|
// Dynamic debug mode check (includes URL parameter)
|
|
12354
12521
|
get debug() {
|
|
@@ -12371,7 +12538,6 @@ const APP_CONFIG = {
|
|
|
12371
12538
|
},
|
|
12372
12539
|
// Unified template model CDN (single compressed model shared by all characters)
|
|
12373
12540
|
flame: {
|
|
12374
|
-
cdnBase: "https://cdn.spatialwalk.cloud/public",
|
|
12375
12541
|
unifiedModelPath: "base_model.pb.gz"
|
|
12376
12542
|
}
|
|
12377
12543
|
};
|
|
@@ -12465,7 +12631,7 @@ const _AnimationPlayer = class _AnimationPlayer {
|
|
|
12465
12631
|
if (this.streamingPlayer) {
|
|
12466
12632
|
return;
|
|
12467
12633
|
}
|
|
12468
|
-
const { StreamingAudioPlayer } = await import("./StreamingAudioPlayer-
|
|
12634
|
+
const { StreamingAudioPlayer } = await import("./StreamingAudioPlayer-Br8UrVgO.js");
|
|
12469
12635
|
const { AvatarSDK: AvatarSDK2 } = await Promise.resolve().then(() => AvatarSDK$1);
|
|
12470
12636
|
const audioFormat = AvatarSDK2.getAudioFormat();
|
|
12471
12637
|
this.streamingPlayer = new StreamingAudioPlayer({
|
|
@@ -14525,7 +14691,7 @@ class AvatarSDK {
|
|
|
14525
14691
|
__publicField(AvatarSDK, "_initializationState", "uninitialized");
|
|
14526
14692
|
__publicField(AvatarSDK, "_initializingPromise", null);
|
|
14527
14693
|
__publicField(AvatarSDK, "_configuration", null);
|
|
14528
|
-
__publicField(AvatarSDK, "_version", "1.
|
|
14694
|
+
__publicField(AvatarSDK, "_version", "1.2.0-beta.1");
|
|
14529
14695
|
__publicField(AvatarSDK, "_avatarCore", null);
|
|
14530
14696
|
__publicField(AvatarSDK, "_cachedDeviceScore", null);
|
|
14531
14697
|
__publicField(AvatarSDK, "_rendererBackend", null);
|
|
@@ -14576,6 +14742,40 @@ class EventEmitter {
|
|
|
14576
14742
|
}
|
|
14577
14743
|
}
|
|
14578
14744
|
}
|
|
14745
|
+
class PendingAudioBuffer {
|
|
14746
|
+
constructor() {
|
|
14747
|
+
__publicField(this, "items", []);
|
|
14748
|
+
}
|
|
14749
|
+
get isEmpty() {
|
|
14750
|
+
return this.items.length === 0;
|
|
14751
|
+
}
|
|
14752
|
+
get count() {
|
|
14753
|
+
return this.items.length;
|
|
14754
|
+
}
|
|
14755
|
+
/** Buffer a chunk sent while not yet able to reach the server. */
|
|
14756
|
+
enqueue(item) {
|
|
14757
|
+
this.items.push(item);
|
|
14758
|
+
}
|
|
14759
|
+
/**
|
|
14760
|
+
* Drain the buffer, returning only the latest conversation's chunks (in
|
|
14761
|
+
* arrival order) for replay and dropping anything from earlier conversations.
|
|
14762
|
+
* The buffer is empty afterwards.
|
|
14763
|
+
*/
|
|
14764
|
+
flush() {
|
|
14765
|
+
const buffered = this.items;
|
|
14766
|
+
this.items = [];
|
|
14767
|
+
if (buffered.length === 0) {
|
|
14768
|
+
return { toReplay: [], dropped: 0 };
|
|
14769
|
+
}
|
|
14770
|
+
const currentId = buffered[buffered.length - 1].conversationId;
|
|
14771
|
+
const toReplay = buffered.filter((it2) => it2.conversationId === currentId);
|
|
14772
|
+
return { toReplay, dropped: buffered.length - toReplay.length };
|
|
14773
|
+
}
|
|
14774
|
+
/** Drop everything (conversation ended / explicit disconnect). */
|
|
14775
|
+
clear() {
|
|
14776
|
+
this.items = [];
|
|
14777
|
+
}
|
|
14778
|
+
}
|
|
14579
14779
|
class AnimationWebSocketClient extends EventEmitter {
|
|
14580
14780
|
constructor(options) {
|
|
14581
14781
|
super();
|
|
@@ -14593,6 +14793,13 @@ class AnimationWebSocketClient extends EventEmitter {
|
|
|
14593
14793
|
__publicField(this, "sessionConfigured", false);
|
|
14594
14794
|
// v2 protocol: mark if session is configured
|
|
14595
14795
|
__publicField(this, "connectionStartTime", 0);
|
|
14796
|
+
/**
|
|
14797
|
+
* Direct mode: audio sent before the session is confirmed is buffered here and
|
|
14798
|
+
* replayed (in order) the moment the session is confirmed, so the server's
|
|
14799
|
+
* audio stream stays identical to what played locally. See PendingAudioBuffer
|
|
14800
|
+
* for the rationale and conversation-boundary handling.
|
|
14801
|
+
*/
|
|
14802
|
+
__publicField(this, "pendingAudioBuffer", new PendingAudioBuffer());
|
|
14596
14803
|
this.wsUrl = options.wsUrl;
|
|
14597
14804
|
this.reconnectAttempts = options.reconnectAttempts ?? 5;
|
|
14598
14805
|
this.jwtToken = options.jwtToken;
|
|
@@ -14637,6 +14844,7 @@ class AnimationWebSocketClient extends EventEmitter {
|
|
|
14637
14844
|
this.ws.close(1e3, "Normal closure");
|
|
14638
14845
|
this.ws = null;
|
|
14639
14846
|
}
|
|
14847
|
+
this.pendingAudioBuffer.clear();
|
|
14640
14848
|
idManager.clearConnectionId();
|
|
14641
14849
|
this.removeAllListeners();
|
|
14642
14850
|
this.currentRetryCount = 0;
|
|
@@ -14655,14 +14863,18 @@ class AnimationWebSocketClient extends EventEmitter {
|
|
|
14655
14863
|
* @internal
|
|
14656
14864
|
*/
|
|
14657
14865
|
sendAudioData(conversationId, audioData, end) {
|
|
14658
|
-
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
14659
|
-
logger.warn("[AnimationWebSocketClient]
|
|
14660
|
-
|
|
14661
|
-
}
|
|
14662
|
-
if (!this.sessionConfigured) {
|
|
14663
|
-
logger.warn("[AnimationWebSocketClient] Session not configured yet, skipping audio send");
|
|
14866
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN || !this.sessionConfigured) {
|
|
14867
|
+
logger.warn("[AnimationWebSocketClient] Not ready (socket/session), buffering audio for replay on connect");
|
|
14868
|
+
this.pendingAudioBuffer.enqueue({ conversationId, audioData, end });
|
|
14664
14869
|
return false;
|
|
14665
14870
|
}
|
|
14871
|
+
return this.encodeAndSendAudio(conversationId, audioData, end);
|
|
14872
|
+
}
|
|
14873
|
+
/**
|
|
14874
|
+
* Encode and write one audio chunk to the open socket. Caller must ensure the
|
|
14875
|
+
* socket is open and the session is configured.
|
|
14876
|
+
*/
|
|
14877
|
+
encodeAndSendAudio(conversationId, audioData, end) {
|
|
14666
14878
|
try {
|
|
14667
14879
|
const message = {
|
|
14668
14880
|
type: MessageType.MESSAGE_CLIENT_AUDIO_INPUT,
|
|
@@ -14689,6 +14901,23 @@ class AnimationWebSocketClient extends EventEmitter {
|
|
|
14689
14901
|
return false;
|
|
14690
14902
|
}
|
|
14691
14903
|
}
|
|
14904
|
+
/**
|
|
14905
|
+
* Replay audio buffered before the session was confirmed, in arrival order,
|
|
14906
|
+
* now that it's confirmed. Only the latest conversation's audio is replayed so
|
|
14907
|
+
* the server's audio stream matches what played locally; earlier conversations
|
|
14908
|
+
* are dropped. Aligned with iOS / Android flushPendingAudioMessages.
|
|
14909
|
+
*/
|
|
14910
|
+
flushPendingAudio() {
|
|
14911
|
+
if (this.pendingAudioBuffer.isEmpty) return;
|
|
14912
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN || !this.sessionConfigured) return;
|
|
14913
|
+
const { toReplay, dropped } = this.pendingAudioBuffer.flush();
|
|
14914
|
+
logger.log(
|
|
14915
|
+
`[AnimationWebSocketClient] Flushing ${toReplay.length} buffered audio chunk(s) to driving service.` + (dropped > 0 ? ` Dropped ${dropped} from earlier conversation(s).` : "")
|
|
14916
|
+
);
|
|
14917
|
+
for (const item of toReplay) {
|
|
14918
|
+
this.encodeAndSendAudio(item.conversationId, item.audioData, item.end);
|
|
14919
|
+
}
|
|
14920
|
+
}
|
|
14692
14921
|
/**
|
|
14693
14922
|
* Generate conversation ID
|
|
14694
14923
|
* Uses unified conversation ID generation rule: YYYYMMDDHHmmss_nanoid
|
|
@@ -14937,6 +15166,7 @@ class AnimationWebSocketClient extends EventEmitter {
|
|
|
14937
15166
|
} else {
|
|
14938
15167
|
logger.log("[AnimationWebSocketClient] Session confirmed by server");
|
|
14939
15168
|
}
|
|
15169
|
+
this.flushPendingAudio();
|
|
14940
15170
|
this.emit("sessionConfirmed", connectionId);
|
|
14941
15171
|
return;
|
|
14942
15172
|
}
|
|
@@ -15212,7 +15442,7 @@ class NetworkLayer {
|
|
|
15212
15442
|
logger.error("[NetworkLayer] Invalid animation message");
|
|
15213
15443
|
return;
|
|
15214
15444
|
}
|
|
15215
|
-
const { reqId, animation, avatarId } = message.serverResponseAnimation;
|
|
15445
|
+
const { reqId, animation, avatarId, end } = message.serverResponseAnimation;
|
|
15216
15446
|
const conversationId = reqId;
|
|
15217
15447
|
if (avatarId && avatarId !== this.dataController.getAvatarId()) {
|
|
15218
15448
|
logger.error(`[NetworkLayer] Ignoring animation data for mismatched avatar - expected: ${this.dataController.getAvatarId()}, received: ${avatarId}`);
|
|
@@ -15239,7 +15469,7 @@ class NetworkLayer {
|
|
|
15239
15469
|
if ((animation == null ? void 0 : animation.keyframes) && animation.keyframes.length > 0) {
|
|
15240
15470
|
const keyframes = animation.keyframes;
|
|
15241
15471
|
try {
|
|
15242
|
-
this.dataController.yieldKeyframes(keyframes, conversationId);
|
|
15472
|
+
this.dataController.yieldKeyframes(keyframes, conversationId, end);
|
|
15243
15473
|
} catch (error) {
|
|
15244
15474
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
15245
15475
|
logger.error(`[NetworkLayer] Failed to yield animation data: ${errorMessage}`);
|
|
@@ -15606,6 +15836,21 @@ class AvatarController {
|
|
|
15606
15836
|
__publicField(this, "onError", null);
|
|
15607
15837
|
/** Callback for animation type changes (e.g., idle → mono in fallback mode). Aligned with iOS/Android AvatarController.onAnimationState. */
|
|
15608
15838
|
__publicField(this, "onAnimationState", null);
|
|
15839
|
+
/**
|
|
15840
|
+
* Strategy for handling animation-frame starvation. Default is
|
|
15841
|
+
* {@link FrameStarvationMode.audioIndependent} (audio keeps playing, starvation only
|
|
15842
|
+
* reported as telemetry — historical behavior). Set to {@link FrameStarvationMode.strictSync}
|
|
15843
|
+
* to pause audio when frames run out and resume on new frames, notified via {@link onPlaybackStall}.
|
|
15844
|
+
* Aligned with iOS/Android AvatarController.frameStarvationMode.
|
|
15845
|
+
*/
|
|
15846
|
+
__publicField(this, "frameStarvationMode", FrameStarvationMode.audioIndependent);
|
|
15847
|
+
/**
|
|
15848
|
+
* Fires when audio is paused/resumed due to frame starvation. Only invoked in
|
|
15849
|
+
* {@link FrameStarvationMode.strictSync}. `stalled=true` — frames ran out, audio paused;
|
|
15850
|
+
* `stalled=false` — new frames arrived, audio resumed (or conversation ended / fell back).
|
|
15851
|
+
* Aligned with iOS/Android AvatarController.onPlaybackStall.
|
|
15852
|
+
*/
|
|
15853
|
+
__publicField(this, "onPlaybackStall", null);
|
|
15609
15854
|
__publicField(this, "eventListeners", /* @__PURE__ */ new Map());
|
|
15610
15855
|
// ========== Performance Monitoring ==========
|
|
15611
15856
|
__publicField(this, "frameRateMonitor", new FrameRateMonitor());
|
|
@@ -15634,6 +15879,18 @@ class AvatarController {
|
|
|
15634
15879
|
// ========== Frame Starvation Tracking ==========
|
|
15635
15880
|
__publicField(this, "frameStarvationEvents", []);
|
|
15636
15881
|
__publicField(this, "isFrameStarved", false);
|
|
15882
|
+
/**
|
|
15883
|
+
* Whether this round's final animation batch (ServerResponseAnimation.end) has arrived.
|
|
15884
|
+
* Frame starvation is only possible BEFORE this — once all frames are in, any remaining
|
|
15885
|
+
* audio tail is normal end-of-round, not starvation, so audio must keep playing to idle
|
|
15886
|
+
* and must never be paused (matters most in strictSync). Reset per conversation.
|
|
15887
|
+
*/
|
|
15888
|
+
__publicField(this, "animationEnded", false);
|
|
15889
|
+
/**
|
|
15890
|
+
* Whether audio is currently paused because of frame starvation (strictSync only).
|
|
15891
|
+
* Orthogonal to user pause — user pause/resume does not change it; only frame arrival does.
|
|
15892
|
+
*/
|
|
15893
|
+
__publicField(this, "isAudioStalledForStarvation", false);
|
|
15637
15894
|
// ========== Playback Stuck Detection ==========
|
|
15638
15895
|
__publicField(this, "playbackStuckCheckState", {
|
|
15639
15896
|
audioTimeZeroCount: 0,
|
|
@@ -16151,7 +16408,7 @@ class AvatarController {
|
|
|
16151
16408
|
if (allKeyframes.length === 0) {
|
|
16152
16409
|
logger.warn(`[AvatarController] No keyframes decoded from ${keyframesDataArray.length} message chunks`);
|
|
16153
16410
|
}
|
|
16154
|
-
this.yieldKeyframes(allKeyframes, conversationId);
|
|
16411
|
+
this.yieldKeyframes(allKeyframes, conversationId, isEnd);
|
|
16155
16412
|
return isEnd;
|
|
16156
16413
|
}
|
|
16157
16414
|
/**
|
|
@@ -16159,12 +16416,15 @@ class AvatarController {
|
|
|
16159
16416
|
* External consumers should use `yieldFramesData()` instead.
|
|
16160
16417
|
* @internal
|
|
16161
16418
|
*/
|
|
16162
|
-
yieldKeyframes(keyframes, conversationId) {
|
|
16163
|
-
var _a;
|
|
16419
|
+
yieldKeyframes(keyframes, conversationId, isEnd = false) {
|
|
16420
|
+
var _a, _b;
|
|
16164
16421
|
if (!conversationId || typeof conversationId !== "string") {
|
|
16165
16422
|
logger.error(`[AvatarController] yieldKeyframes requires a valid conversationId. The conversationId is returned by yieldAudioData().`);
|
|
16166
16423
|
return;
|
|
16167
16424
|
}
|
|
16425
|
+
if (isEnd) {
|
|
16426
|
+
this.animationEnded = true;
|
|
16427
|
+
}
|
|
16168
16428
|
const expectedConversationId = this.getEffectiveConversationId();
|
|
16169
16429
|
if (!expectedConversationId || conversationId !== expectedConversationId) {
|
|
16170
16430
|
logger.warn(`[AvatarController] Ignoring mismatched animation data - expected conversationId: ${expectedConversationId}, received conversationId: ${conversationId}`);
|
|
@@ -16194,6 +16454,14 @@ class AvatarController {
|
|
|
16194
16454
|
this.currentKeyframes.push(...flameKeyframes);
|
|
16195
16455
|
}
|
|
16196
16456
|
this.emit("keyframesUpdate", this.currentKeyframes);
|
|
16457
|
+
if (this.isAudioStalledForStarvation) {
|
|
16458
|
+
const audioTime = ((_b = this.animationPlayer) == null ? void 0 : _b.getCurrentTime()) ?? 0;
|
|
16459
|
+
const frameIndex = Math.round(audioTime * FLAME_FRAME_RATE);
|
|
16460
|
+
const arrayIndex = frameIndex - this.keyframesOffset;
|
|
16461
|
+
if (arrayIndex >= 0 && arrayIndex < this.currentKeyframes.length) {
|
|
16462
|
+
this.resumeAudioFromStarvation();
|
|
16463
|
+
}
|
|
16464
|
+
}
|
|
16197
16465
|
if (!this.isPlaying && !this.isStartingPlayback && this.pendingAudioChunks.length > 0 && this.currentKeyframes.length > 0) {
|
|
16198
16466
|
this.startStreamingPlayback().catch((error) => {
|
|
16199
16467
|
var _a2;
|
|
@@ -16230,7 +16498,9 @@ class AvatarController {
|
|
|
16230
16498
|
return;
|
|
16231
16499
|
}
|
|
16232
16500
|
logger.log("[AvatarController] Resuming playback");
|
|
16233
|
-
|
|
16501
|
+
if (!this.isAudioStalledForStarvation) {
|
|
16502
|
+
await ((_a = this.animationPlayer) == null ? void 0 : _a.resume());
|
|
16503
|
+
}
|
|
16234
16504
|
this.currentState = AvatarState.playing;
|
|
16235
16505
|
this.notifyConversationState(AvatarState.playing);
|
|
16236
16506
|
logger.log("[AvatarController] Playback resumed");
|
|
@@ -16305,6 +16575,7 @@ class AvatarController {
|
|
|
16305
16575
|
* @internal
|
|
16306
16576
|
*/
|
|
16307
16577
|
clearPlaybackData() {
|
|
16578
|
+
var _a;
|
|
16308
16579
|
this.currentKeyframes = [];
|
|
16309
16580
|
this.pendingAudioChunks = [];
|
|
16310
16581
|
this.lastRenderedFrameIndex = -1;
|
|
@@ -16312,6 +16583,11 @@ class AvatarController {
|
|
|
16312
16583
|
this.isFallbackMode = false;
|
|
16313
16584
|
this.lastSyncLogTime = 0;
|
|
16314
16585
|
this.lastOutOfBoundsState = false;
|
|
16586
|
+
this.animationEnded = false;
|
|
16587
|
+
if (this.isAudioStalledForStarvation) {
|
|
16588
|
+
this.isAudioStalledForStarvation = false;
|
|
16589
|
+
(_a = this.onPlaybackStall) == null ? void 0 : _a.call(this, false);
|
|
16590
|
+
}
|
|
16315
16591
|
if (this.playbackMode === DrivingServiceMode.backend) {
|
|
16316
16592
|
this.hostModeMetrics = {
|
|
16317
16593
|
accumulatedBytes: 0,
|
|
@@ -16574,6 +16850,13 @@ class AvatarController {
|
|
|
16574
16850
|
const hasAnimationData = this.currentKeyframes.length > 0;
|
|
16575
16851
|
const hasAudioData = ((_b = streamingPlayer.audioChunks) == null ? void 0 : _b.length) > 0;
|
|
16576
16852
|
const isNotPaused = this.currentState !== AvatarState.paused;
|
|
16853
|
+
if (this.isAudioStalledForStarvation) {
|
|
16854
|
+
state.audioTimeZeroCount = 0;
|
|
16855
|
+
state.audioTimeStuckCount = 0;
|
|
16856
|
+
state.lastAudioTime = 0;
|
|
16857
|
+
state.reported = false;
|
|
16858
|
+
return false;
|
|
16859
|
+
}
|
|
16577
16860
|
if (!hasAnimationData || !hasAudioData || this.currentState === AvatarState.paused) {
|
|
16578
16861
|
state.audioTimeZeroCount = 0;
|
|
16579
16862
|
state.audioTimeStuckCount = 0;
|
|
@@ -16677,13 +16960,49 @@ class AvatarController {
|
|
|
16677
16960
|
this.frameStarvationEvents.push({ audioTime, reqEnd: this.reqEnd });
|
|
16678
16961
|
}
|
|
16679
16962
|
}
|
|
16963
|
+
if (!this.animationEnded) {
|
|
16964
|
+
this.pauseAudioForStarvation();
|
|
16965
|
+
}
|
|
16680
16966
|
} else {
|
|
16681
16967
|
this.isFrameStarved = false;
|
|
16682
16968
|
if (isOutOfBounds !== this.lastOutOfBoundsState) {
|
|
16683
16969
|
this.lastOutOfBoundsState = isOutOfBounds;
|
|
16684
16970
|
}
|
|
16971
|
+
this.resumeAudioFromStarvation();
|
|
16685
16972
|
}
|
|
16686
16973
|
}
|
|
16974
|
+
/**
|
|
16975
|
+
* strictSync: pause audio on frame starvation, waiting for new frames.
|
|
16976
|
+
*
|
|
16977
|
+
* The default (audioIndependent) keeps audio running and lets animation catch up;
|
|
16978
|
+
* strictSync instead pauses audio so audio/video stay strictly in sync. Idempotent;
|
|
16979
|
+
* only fires onPlaybackStall once. No-op unless mode is strictSync.
|
|
16980
|
+
* @internal
|
|
16981
|
+
*/
|
|
16982
|
+
pauseAudioForStarvation() {
|
|
16983
|
+
var _a, _b;
|
|
16984
|
+
if (this.frameStarvationMode !== FrameStarvationMode.strictSync) return;
|
|
16985
|
+
if (this.isAudioStalledForStarvation) return;
|
|
16986
|
+
if (!this.isPlaying) return;
|
|
16987
|
+
(_a = this.animationPlayer) == null ? void 0 : _a.pause();
|
|
16988
|
+
this.isAudioStalledForStarvation = true;
|
|
16989
|
+
logger.log("[AvatarController] Frame starvation: paused audio, waiting for frames.");
|
|
16990
|
+
(_b = this.onPlaybackStall) == null ? void 0 : _b.call(this, true);
|
|
16991
|
+
}
|
|
16992
|
+
/**
|
|
16993
|
+
* strictSync: frame starvation resolved (new frames arrived). Resumes audio and fires
|
|
16994
|
+
* onPlaybackStall(false). Idempotent — returns immediately if not stalled (so it is a
|
|
16995
|
+
* no-op in audioIndependent, where isAudioStalledForStarvation stays false).
|
|
16996
|
+
* @internal
|
|
16997
|
+
*/
|
|
16998
|
+
resumeAudioFromStarvation() {
|
|
16999
|
+
var _a, _b;
|
|
17000
|
+
if (!this.isAudioStalledForStarvation) return;
|
|
17001
|
+
this.isAudioStalledForStarvation = false;
|
|
17002
|
+
void ((_a = this.animationPlayer) == null ? void 0 : _a.resume());
|
|
17003
|
+
logger.log("[AvatarController] Frame starvation recovered: resumed audio.");
|
|
17004
|
+
(_b = this.onPlaybackStall) == null ? void 0 : _b.call(this, false);
|
|
17005
|
+
}
|
|
16687
17006
|
/**
|
|
16688
17007
|
* Start playback loop
|
|
16689
17008
|
* @internal
|
|
@@ -16729,6 +17048,7 @@ class AvatarController {
|
|
|
16729
17048
|
}
|
|
16730
17049
|
logger.warn("[AvatarController] Enabling fallback mode");
|
|
16731
17050
|
this.isFallbackMode = true;
|
|
17051
|
+
this.resumeAudioFromStarvation();
|
|
16732
17052
|
logEvent("fallback_mode_entered", "warning", {
|
|
16733
17053
|
avatar_id: this.avatar.id,
|
|
16734
17054
|
reason,
|
|
@@ -17253,9 +17573,12 @@ class AvatarDownloader {
|
|
|
17253
17573
|
* @internal
|
|
17254
17574
|
*/
|
|
17255
17575
|
async loadUnifiedTemplate() {
|
|
17576
|
+
var _a;
|
|
17256
17577
|
await PwaCacheManager.checkTemplateCacheVersion();
|
|
17257
17578
|
const startTime = Date.now();
|
|
17258
|
-
const
|
|
17579
|
+
const region = ((_a = AvatarSDK.configuration) == null ? void 0 : _a.region) || DEFAULT_REGION;
|
|
17580
|
+
const cdnBase = getFlameCdnBase(region);
|
|
17581
|
+
const { unifiedModelPath } = APP_CONFIG.flame;
|
|
17259
17582
|
const url = `${cdnBase}/${unifiedModelPath}`;
|
|
17260
17583
|
logger.log(`📥 Loading unified template from: ${url}`);
|
|
17261
17584
|
const cached = await PwaCacheManager.getTemplateResource(url);
|
|
@@ -17526,7 +17849,7 @@ class AvatarDownloader {
|
|
|
17526
17849
|
}
|
|
17527
17850
|
let error;
|
|
17528
17851
|
if (response.status === 404) {
|
|
17529
|
-
const urlMatch = url.match(/\/v2\/character\/([^/?]+)/);
|
|
17852
|
+
const urlMatch = url.match(/\/v2\/(?:character|avatar)\/([^/?]+)/);
|
|
17530
17853
|
const extractedCharacterId = urlMatch ? urlMatch[1] : "unknown";
|
|
17531
17854
|
const callerHeaders = options.headers || {};
|
|
17532
17855
|
const callerTraceId = callerHeaders["x-sp-trace-id"];
|
|
@@ -17562,10 +17885,50 @@ class AvatarDownloader {
|
|
|
17562
17885
|
};
|
|
17563
17886
|
}
|
|
17564
17887
|
/**
|
|
17565
|
-
*
|
|
17888
|
+
* Map the new `/v2/avatar/{id}` `AvatarAsset` payload onto the internal
|
|
17889
|
+
* `CharacterMeta` shape used by the existing download / render pipeline.
|
|
17890
|
+
*
|
|
17891
|
+
* The backend (grpc-gateway) serialises proto fields as camelCase JSON, so the
|
|
17892
|
+
* runtime object is loosely shaped like the generated `AvatarAsset`. We:
|
|
17893
|
+
* - lift `models.gs` into `models.gsStandard` (downloader/asset-count read gsStandard)
|
|
17894
|
+
* - rename `animations.frameFallback` → `animations.frameMono`
|
|
17895
|
+
* - fold the inline `camera` / `transform` into `characterSettings` so the
|
|
17896
|
+
* renderer's `resolveCameraConfig` reads structured values and no camera
|
|
17897
|
+
* resource is downloaded (top-level `camera` is intentionally left unset)
|
|
17898
|
+
* @internal
|
|
17899
|
+
*/
|
|
17900
|
+
mapAvatarAssetToCharacterMeta(asset, avatarId) {
|
|
17901
|
+
var _a, _b, _c, _d, _e2;
|
|
17902
|
+
const characterSettings = {
|
|
17903
|
+
...asset.camera ? { camera: { ...asset.camera } } : {},
|
|
17904
|
+
...asset.transform ? { transform: { ...asset.transform } } : {}
|
|
17905
|
+
};
|
|
17906
|
+
return {
|
|
17907
|
+
characterId: avatarId,
|
|
17908
|
+
version: asset.version ?? "",
|
|
17909
|
+
updatedAt: asset.updatedAt,
|
|
17910
|
+
models: {
|
|
17911
|
+
shape: (_a = asset.models) == null ? void 0 : _a.shape,
|
|
17912
|
+
gsStandard: (_b = asset.models) == null ? void 0 : _b.gs
|
|
17913
|
+
},
|
|
17914
|
+
animations: {
|
|
17915
|
+
frameIdle: (_c = asset.animations) == null ? void 0 : _c.frameIdle,
|
|
17916
|
+
frameMono: (_d = asset.animations) == null ? void 0 : _d.frameFallback
|
|
17917
|
+
},
|
|
17918
|
+
customAnimations: ((_e2 = asset.animations) == null ? void 0 : _e2.customAnimations) ?? [],
|
|
17919
|
+
characterSettings
|
|
17920
|
+
};
|
|
17921
|
+
}
|
|
17922
|
+
/**
|
|
17923
|
+
* Get single avatar by ID from AvatarKit SDK API (v2 driven-ingress avatar API).
|
|
17566
17924
|
* Domain: composed from region as api.${region}.spatius.ai
|
|
17567
17925
|
* Auth: Public endpoint, no authentication required
|
|
17568
|
-
*
|
|
17926
|
+
* Fetches the new `AvatarAsset` payload from `/v2/avatar/{id}` and maps it onto
|
|
17927
|
+
* the internal `CharacterMeta` shape consumed by the download / render pipeline:
|
|
17928
|
+
* - `models.gs` → `models.gsStandard`
|
|
17929
|
+
* - `animations.frameFallback` → `animations.frameMono`
|
|
17930
|
+
* - inline `camera` / `transform` → `characterSettings.{camera,transform}`
|
|
17931
|
+
* (so the renderer reads structured values directly and no camera resource is downloaded)
|
|
17569
17932
|
* @internal
|
|
17570
17933
|
*/
|
|
17571
17934
|
async getCharacterById(characterId, options) {
|
|
@@ -17578,7 +17941,7 @@ class AvatarDownloader {
|
|
|
17578
17941
|
throw new Error("Request cancelled");
|
|
17579
17942
|
}
|
|
17580
17943
|
const client = this.getSdkApiClient();
|
|
17581
|
-
const response = await client.request(`/v2/
|
|
17944
|
+
const response = await client.request(`/v2/avatar/${characterId}`, {
|
|
17582
17945
|
method: "GET",
|
|
17583
17946
|
headers: { "x-sp-trace-id": traceId },
|
|
17584
17947
|
signal
|
|
@@ -17597,7 +17960,7 @@ class AvatarDownloader {
|
|
|
17597
17960
|
duration,
|
|
17598
17961
|
trace_id: traceId
|
|
17599
17962
|
});
|
|
17600
|
-
return response;
|
|
17963
|
+
return this.mapAvatarAssetToCharacterMeta(response, characterId);
|
|
17601
17964
|
} catch (error) {
|
|
17602
17965
|
if (error instanceof Error && (error.name === "AbortError" || error.message === "Request cancelled")) {
|
|
17603
17966
|
logEvent("fetch_avatar_metadata_cancelled", "info", {
|
|
@@ -20793,6 +21156,10 @@ class AvatarView {
|
|
|
20793
21156
|
},
|
|
20794
21157
|
onPlayFallback: () => {
|
|
20795
21158
|
this.playAnimation(AnimationType.idle, true);
|
|
21159
|
+
if (!this.cachedIdleFirstFrame) {
|
|
21160
|
+
this.getCachedIdleFirstFrame().catch(() => {
|
|
21161
|
+
});
|
|
21162
|
+
}
|
|
20796
21163
|
this.isConversationActive = true;
|
|
20797
21164
|
this.lastRenderedFrameIndex = -1;
|
|
20798
21165
|
},
|
|
@@ -21465,6 +21832,7 @@ export {
|
|
|
21465
21832
|
ConnectionState as C,
|
|
21466
21833
|
DrivingServiceMode as D,
|
|
21467
21834
|
ErrorCode as E,
|
|
21835
|
+
FrameStarvationMode as F,
|
|
21468
21836
|
LogLevel as L,
|
|
21469
21837
|
RenderQuality as R,
|
|
21470
21838
|
TransitionType as T,
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { k, b, c, o, f, d, n, g, C, m, i, D, E, j, L, h, R, p, T, q } from "./index-
|
|
1
|
+
import { k, b, c, o, f, d, n, g, C, m, i, D, E, F, j, L, h, R, p, T, q } from "./index-DHCQ9wsL.js";
|
|
2
2
|
export {
|
|
3
3
|
k as AnimationType,
|
|
4
4
|
b as Avatar,
|
|
@@ -13,6 +13,7 @@ export {
|
|
|
13
13
|
i as DEFAULT_REGION,
|
|
14
14
|
D as DrivingServiceMode,
|
|
15
15
|
E as ErrorCode,
|
|
16
|
+
F as FrameStarvationMode,
|
|
16
17
|
j as LoadProgress,
|
|
17
18
|
L as LogLevel,
|
|
18
19
|
h as RENDER_QUALITY_PARAMS,
|
package/dist/types/index.d.ts
CHANGED
|
@@ -7,6 +7,19 @@ export declare enum DrivingServiceMode {
|
|
|
7
7
|
/** Driven by host application */
|
|
8
8
|
backend = "backend"
|
|
9
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* Strategy for handling animation-frame starvation (animation frames can't keep up
|
|
12
|
+
* with the audio clock).
|
|
13
|
+
*
|
|
14
|
+
* - `audioIndependent` (default): audio keeps playing, animation catches up; starvation
|
|
15
|
+
* is only reported as telemetry. This is the historical default behavior.
|
|
16
|
+
* - `strictSync`: pause audio and wait when frames run out, resume once new frames
|
|
17
|
+
* arrive, notifying via `AvatarController.onPlaybackStall`.
|
|
18
|
+
*/
|
|
19
|
+
export declare enum FrameStarvationMode {
|
|
20
|
+
audioIndependent = "audioIndependent",
|
|
21
|
+
strictSync = "strictSync"
|
|
22
|
+
}
|
|
10
23
|
export declare enum LogLevel {
|
|
11
24
|
/** Disable all logs */
|
|
12
25
|
off = "off",
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spatius/avatarkit",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.2.0-beta.1",
|
|
5
5
|
"packageManager": "pnpm@10.18.2",
|
|
6
6
|
"description": "AvatarKit SDK - 3D Gaussian Splatting Avatar Rendering SDK",
|
|
7
7
|
"author": "AvatarKit Team",
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
"build": "SDK_BUILD=true vite build --mode library && npm run build:vite-plugin && npm run build:next-plugin",
|
|
49
49
|
"build:vite-plugin": "tsc vite.ts --outDir . --module esnext --target es2020 --moduleResolution bundler --esModuleInterop --skipLibCheck --declaration --declarationMap",
|
|
50
50
|
"build:next-plugin": "tsc next.ts --outDir . --module esnext --target es2020 --moduleResolution bundler --esModuleInterop --skipLibCheck --declaration --declarationMap",
|
|
51
|
+
"prepare": "npm run build",
|
|
51
52
|
"dev": "vite build --mode library --watch",
|
|
52
53
|
"demo": "vite --config demo/vite.config.mjs",
|
|
53
54
|
"demo:benchmark": "vite --config benchmark-demo/vite.config.mjs",
|