@spatialwalk/avatarkit-rtc 1.0.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/README.md +417 -0
- package/dist/assets/animation-worker-CUXZycUw.js.map +1 -0
- package/dist/core/AnimationHandler.d.ts +17 -0
- package/dist/core/AnimationHandler.d.ts.map +1 -0
- package/dist/core/AvatarPlayer.d.ts +119 -0
- package/dist/core/AvatarPlayer.d.ts.map +1 -0
- package/dist/core/RTCProvider.d.ts +84 -0
- package/dist/core/RTCProvider.d.ts.map +1 -0
- package/dist/core/index.d.ts +7 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/types.d.ts +7 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/index10.js +67 -0
- package/dist/index10.js.map +1 -0
- package/dist/index11.js +390 -0
- package/dist/index11.js.map +1 -0
- package/dist/index12.js +108 -0
- package/dist/index12.js.map +1 -0
- package/dist/index13.js +18 -0
- package/dist/index13.js.map +1 -0
- package/dist/index14.js +48 -0
- package/dist/index14.js.map +1 -0
- package/dist/index15.js +29 -0
- package/dist/index15.js.map +1 -0
- package/dist/index16.js +144 -0
- package/dist/index16.js.map +1 -0
- package/dist/index17.js +106 -0
- package/dist/index17.js.map +1 -0
- package/dist/index18.js +28 -0
- package/dist/index18.js.map +1 -0
- package/dist/index2.js +220 -0
- package/dist/index2.js.map +1 -0
- package/dist/index3.js +586 -0
- package/dist/index3.js.map +1 -0
- package/dist/index4.js +410 -0
- package/dist/index4.js.map +1 -0
- package/dist/index5.js +20 -0
- package/dist/index5.js.map +1 -0
- package/dist/index6.js +282 -0
- package/dist/index6.js.map +1 -0
- package/dist/index7.js +53 -0
- package/dist/index7.js.map +1 -0
- package/dist/index8.js +189 -0
- package/dist/index8.js.map +1 -0
- package/dist/index9.js +178 -0
- package/dist/index9.js.map +1 -0
- package/dist/proto/animation.d.ts +12 -0
- package/dist/proto/animation.d.ts.map +1 -0
- package/dist/providers/agora/AgoraProvider.d.ts +71 -0
- package/dist/providers/agora/AgoraProvider.d.ts.map +1 -0
- package/dist/providers/agora/SEIExtractor.d.ts +29 -0
- package/dist/providers/agora/SEIExtractor.d.ts.map +1 -0
- package/dist/providers/agora/index.d.ts +11 -0
- package/dist/providers/agora/index.d.ts.map +1 -0
- package/dist/providers/agora/types.d.ts +14 -0
- package/dist/providers/agora/types.d.ts.map +1 -0
- package/dist/providers/base/BaseProvider.d.ts +11 -0
- package/dist/providers/base/BaseProvider.d.ts.map +1 -0
- package/dist/providers/index.d.ts +10 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/livekit/LiveKitProvider.d.ts +64 -0
- package/dist/providers/livekit/LiveKitProvider.d.ts.map +1 -0
- package/dist/providers/livekit/VP8Extractor.d.ts +10 -0
- package/dist/providers/livekit/VP8Extractor.d.ts.map +1 -0
- package/dist/providers/livekit/animation-transform.d.ts +11 -0
- package/dist/providers/livekit/animation-transform.d.ts.map +1 -0
- package/dist/providers/livekit/animation-worker.d.ts +14 -0
- package/dist/providers/livekit/animation-worker.d.ts.map +1 -0
- package/dist/providers/livekit/index.d.ts +11 -0
- package/dist/providers/livekit/index.d.ts.map +1 -0
- package/dist/providers/livekit/types.d.ts +11 -0
- package/dist/providers/livekit/types.d.ts.map +1 -0
- package/dist/providers/livekit/utils.d.ts +11 -0
- package/dist/providers/livekit/utils.d.ts.map +1 -0
- package/dist/types/index.d.ts +77 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/utils/index.d.ts +7 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/logger.d.ts +13 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/package.json +61 -0
package/README.md
ADDED
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
# @spatialwalk/avatarkit-rtc
|
|
2
|
+
|
|
3
|
+
Unified RTC adapter for avatarkit - supports LiveKit, Agora and other RTC providers.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @spatialwalk/avatarkit-rtc
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
### Peer Dependencies
|
|
12
|
+
|
|
13
|
+
根据使用的 RTC 提供商安装对应的 SDK:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# LiveKit
|
|
17
|
+
pnpm add livekit-client
|
|
18
|
+
|
|
19
|
+
# Agora
|
|
20
|
+
pnpm add agora-rtc-sdk-ng
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
import { AvatarPlayer, LiveKitProvider, AgoraProvider } from '@spatialwalk/avatarkit-rtc';
|
|
27
|
+
import { AvatarView, Avatar } from '@spatialwalk/avatarkit';
|
|
28
|
+
|
|
29
|
+
// 1. 创建 Avatar 和 AvatarView(来自 avatarkit)
|
|
30
|
+
const avatar = await Avatar.create(characterId);
|
|
31
|
+
const avatarView = new AvatarView(avatar, container);
|
|
32
|
+
|
|
33
|
+
// 2. 创建 Provider(选择 LiveKit 或 Agora)
|
|
34
|
+
const provider = new LiveKitProvider(); // 或 new AgoraProvider()
|
|
35
|
+
|
|
36
|
+
// 3. 创建 Player
|
|
37
|
+
const player = new AvatarPlayer(provider, avatarView, {
|
|
38
|
+
logLevel: 'warning', // 可选:设置日志级别
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// 4. 连接到 RTC 服务器
|
|
42
|
+
await player.connect({
|
|
43
|
+
url: 'wss://your-livekit-server.com',
|
|
44
|
+
token: 'your-token',
|
|
45
|
+
roomName: 'room-name',
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// 5. 开始发布麦克风音频(用户说话)
|
|
49
|
+
await player.startPublishing();
|
|
50
|
+
|
|
51
|
+
// 6. 停止发布
|
|
52
|
+
await player.stopPublishing();
|
|
53
|
+
|
|
54
|
+
// 7. 断开连接
|
|
55
|
+
await player.disconnect();
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## API Reference
|
|
59
|
+
|
|
60
|
+
### AvatarPlayer
|
|
61
|
+
|
|
62
|
+
主入口类,统一管理 RTC 连接和 Avatar 渲染。
|
|
63
|
+
|
|
64
|
+
#### Constructor
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
new AvatarPlayer(provider: RTCProvider, avatarView: AvatarView, options?: AvatarPlayerOptions)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**参数:**
|
|
71
|
+
|
|
72
|
+
| 参数 | 类型 | 描述 |
|
|
73
|
+
|------|------|------|
|
|
74
|
+
| `provider` | `LiveKitProvider \| AgoraProvider` | RTC 提供商实例 |
|
|
75
|
+
| `avatarView` | `AvatarView` | avatarkit 的 AvatarView 实例 |
|
|
76
|
+
| `options` | `AvatarPlayerOptions` | 可选配置 |
|
|
77
|
+
|
|
78
|
+
#### AvatarPlayerOptions
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
interface AvatarPlayerOptions {
|
|
82
|
+
/** 开始说话过渡帧数,默认 5 (~200ms at 25fps) */
|
|
83
|
+
transitionStartFrameCount?: number;
|
|
84
|
+
|
|
85
|
+
/** 结束说话过渡帧数,默认 40 (~1600ms at 25fps) */
|
|
86
|
+
transitionEndFrameCount?: number;
|
|
87
|
+
|
|
88
|
+
/** 日志级别:'info' | 'warning' | 'error' | 'none',默认 'warning' */
|
|
89
|
+
logLevel?: LogLevel;
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
#### Properties
|
|
94
|
+
|
|
95
|
+
| 属性 | 类型 | 描述 |
|
|
96
|
+
|------|------|------|
|
|
97
|
+
| `isConnected` | `boolean` | 是否已连接到 RTC 服务器 |
|
|
98
|
+
|
|
99
|
+
#### Methods
|
|
100
|
+
|
|
101
|
+
##### connect(config)
|
|
102
|
+
|
|
103
|
+
连接到 RTC 服务器。
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
await player.connect(config: RTCConnectionConfig): Promise<void>
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**LiveKit 配置:**
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
interface LiveKitConnectionConfig {
|
|
113
|
+
url: string; // LiveKit 服务器 URL (wss://...)
|
|
114
|
+
token: string; // 认证 Token
|
|
115
|
+
roomName: string; // 房间名称
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**Agora 配置:**
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
interface AgoraConnectionConfig {
|
|
123
|
+
appId: string; // Agora App ID
|
|
124
|
+
channel: string; // 频道名称
|
|
125
|
+
token?: string; // 认证 Token(生产环境必需)
|
|
126
|
+
uid?: number; // 用户 ID(可选,0 = 自动分配)
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
##### disconnect()
|
|
131
|
+
|
|
132
|
+
断开 RTC 连接,清理所有资源。
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
await player.disconnect(): Promise<void>
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
##### startPublishing()
|
|
139
|
+
|
|
140
|
+
开始发布麦克风音频。会请求麦克风权限。
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
await player.startPublishing(): Promise<void>
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
##### stopPublishing()
|
|
147
|
+
|
|
148
|
+
停止发布麦克风音频。
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
await player.stopPublishing(): Promise<void>
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
##### getConnectionState()
|
|
155
|
+
|
|
156
|
+
获取当前连接状态。
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
player.getConnectionState(): ConnectionState
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
**ConnectionState 枚举:**
|
|
163
|
+
|
|
164
|
+
| 值 | 描述 |
|
|
165
|
+
|----|------|
|
|
166
|
+
| `'disconnected'` | 未连接 |
|
|
167
|
+
| `'connecting'` | 正在连接 |
|
|
168
|
+
| `'connected'` | 已连接 |
|
|
169
|
+
| `'reconnecting'` | 正在重连 |
|
|
170
|
+
| `'failed'` | 连接失败 |
|
|
171
|
+
|
|
172
|
+
##### getNativeClient()
|
|
173
|
+
|
|
174
|
+
获取底层 RTC 客户端对象,用于访问平台特定功能。
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
player.getNativeClient(): unknown
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
**返回值:**
|
|
181
|
+
|
|
182
|
+
| Provider | 返回类型 |
|
|
183
|
+
|----------|---------|
|
|
184
|
+
| LiveKit | `Room` 实例(SDK 导出为 `LiveKitRoom`) |
|
|
185
|
+
| Agora | `IAgoraRTCClient` 实例(SDK 导出为 `AgoraClient`) |
|
|
186
|
+
|
|
187
|
+
**类型安全说明:**
|
|
188
|
+
|
|
189
|
+
- **直接使用 Provider**:返回具体类型,无需断言
|
|
190
|
+
- **通过 AvatarPlayer 使用**:返回 `unknown`,需要手动类型断言
|
|
191
|
+
|
|
192
|
+
**示例:**
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
// 方式 1:直接使用 Provider(推荐,有完整类型提示)
|
|
196
|
+
import { LiveKitProvider, LiveKitRoom } from '@spatialwalk/avatarkit-rtc';
|
|
197
|
+
|
|
198
|
+
const provider = new LiveKitProvider();
|
|
199
|
+
const room = provider.getNativeClient(); // 类型: LiveKitRoom | null
|
|
200
|
+
console.log('远程参与者数量:', room?.remoteParticipants.size);
|
|
201
|
+
|
|
202
|
+
// 方式 2:通过 AvatarPlayer 使用(需手动断言)
|
|
203
|
+
import { AvatarPlayer, LiveKitRoom } from '@spatialwalk/avatarkit-rtc';
|
|
204
|
+
|
|
205
|
+
const room = player.getNativeClient() as LiveKitRoom | null;
|
|
206
|
+
console.log('远程参与者数量:', room?.remoteParticipants.size);
|
|
207
|
+
|
|
208
|
+
// Agora 示例
|
|
209
|
+
import { AgoraProvider, AgoraClient } from '@spatialwalk/avatarkit-rtc';
|
|
210
|
+
|
|
211
|
+
const provider = new AgoraProvider();
|
|
212
|
+
const client = provider.getNativeClient(); // 类型: AgoraClient | null
|
|
213
|
+
console.log('连接状态:', client?.connectionState);
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
##### on(event, handler)
|
|
217
|
+
|
|
218
|
+
监听事件。
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
player.on(event: string, handler: Function): void
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
**支持的事件:**
|
|
225
|
+
|
|
226
|
+
| 事件名 | 回调参数 | 描述 |
|
|
227
|
+
|--------|----------|------|
|
|
228
|
+
| `'connected'` | `()` | 连接成功 |
|
|
229
|
+
| `'disconnected'` | `()` | 连接断开 |
|
|
230
|
+
| `'error'` | `(error: Error)` | 发生错误 |
|
|
231
|
+
| `'connection-state-changed'` | `(state: ConnectionState)` | 连接状态变化 |
|
|
232
|
+
|
|
233
|
+
##### off(event, handler)
|
|
234
|
+
|
|
235
|
+
移除事件监听。
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
player.off(event: string, handler: Function): void
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
### LiveKitProvider
|
|
244
|
+
|
|
245
|
+
LiveKit RTC 提供商实现。
|
|
246
|
+
|
|
247
|
+
```typescript
|
|
248
|
+
import { LiveKitProvider } from '@spatialwalk/avatarkit-rtc';
|
|
249
|
+
|
|
250
|
+
const provider = new LiveKitProvider();
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
### AgoraProvider
|
|
256
|
+
|
|
257
|
+
Agora RTC 提供商实现。
|
|
258
|
+
|
|
259
|
+
```typescript
|
|
260
|
+
import { AgoraProvider } from '@spatialwalk/avatarkit-rtc';
|
|
261
|
+
|
|
262
|
+
const provider = new AgoraProvider({
|
|
263
|
+
debugLogging: true, // 可选:启用调试日志
|
|
264
|
+
});
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
**AgoraProviderOptions:**
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
interface AgoraProviderOptions {
|
|
271
|
+
/** 启用详细调试日志,默认 false */
|
|
272
|
+
debugLogging?: boolean;
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
### Type Guards
|
|
279
|
+
|
|
280
|
+
类型守卫函数,用于判断配置类型。
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
import { isLiveKitConfig, isAgoraConfig } from '@spatialwalk/avatarkit-rtc';
|
|
284
|
+
|
|
285
|
+
if (isLiveKitConfig(config)) {
|
|
286
|
+
// config 是 LiveKitConnectionConfig
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (isAgoraConfig(config)) {
|
|
290
|
+
// config 是 AgoraConnectionConfig
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
---
|
|
295
|
+
|
|
296
|
+
### Native Client Types
|
|
297
|
+
|
|
298
|
+
SDK 导出了底层 RTC 客户端的类型别名,方便在使用 `getNativeClient()` 时获得类型支持:
|
|
299
|
+
|
|
300
|
+
```typescript
|
|
301
|
+
import type { LiveKitRoom, AgoraClient } from '@spatialwalk/avatarkit-rtc';
|
|
302
|
+
|
|
303
|
+
// LiveKitRoom = livekit-client 的 Room 类型
|
|
304
|
+
// AgoraClient = agora-rtc-sdk-ng 的 IAgoraRTCClient 类型
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
|
|
309
|
+
## Log Levels
|
|
310
|
+
|
|
311
|
+
SDK 日志级别通过 `AvatarPlayerOptions.logLevel` 配置:
|
|
312
|
+
|
|
313
|
+
| Level | 输出内容 |
|
|
314
|
+
|-------|---------|
|
|
315
|
+
| `'info'` | 所有日志(连接状态、帧处理、调试信息) |
|
|
316
|
+
| `'warning'` | 警告 + 错误(**默认**) |
|
|
317
|
+
| `'error'` | 只有错误 |
|
|
318
|
+
| `'none'` | 禁用所有日志 |
|
|
319
|
+
|
|
320
|
+
```typescript
|
|
321
|
+
const player = new AvatarPlayer(provider, avatarView, {
|
|
322
|
+
logLevel: 'info', // 调试时启用
|
|
323
|
+
});
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
---
|
|
327
|
+
|
|
328
|
+
## Complete Example
|
|
329
|
+
|
|
330
|
+
### LiveKit 接入
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
import { AvatarPlayer, LiveKitProvider } from '@spatialwalk/avatarkit-rtc';
|
|
334
|
+
import { AvatarView, Avatar } from '@spatialwalk/avatarkit';
|
|
335
|
+
|
|
336
|
+
async function initLiveKit() {
|
|
337
|
+
// 初始化 Avatar
|
|
338
|
+
const avatar = await Avatar.create('character-id');
|
|
339
|
+
const container = document.getElementById('avatar-container')!;
|
|
340
|
+
const avatarView = new AvatarView(avatar, container);
|
|
341
|
+
|
|
342
|
+
// 创建 Player
|
|
343
|
+
const provider = new LiveKitProvider();
|
|
344
|
+
const player = new AvatarPlayer(provider, avatarView, {
|
|
345
|
+
logLevel: 'info',
|
|
346
|
+
transitionStartFrameCount: 5,
|
|
347
|
+
transitionEndFrameCount: 40,
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
// 监听事件
|
|
351
|
+
player.on('connected', () => console.log('Connected!'));
|
|
352
|
+
player.on('disconnected', () => console.log('Disconnected!'));
|
|
353
|
+
player.on('error', (err) => console.error('Error:', err));
|
|
354
|
+
|
|
355
|
+
// 连接
|
|
356
|
+
await player.connect({
|
|
357
|
+
url: 'wss://your-livekit-server.com',
|
|
358
|
+
token: 'your-token',
|
|
359
|
+
roomName: 'my-room',
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
// 开始说话
|
|
363
|
+
await player.startPublishing();
|
|
364
|
+
|
|
365
|
+
// 停止说话
|
|
366
|
+
// await player.stopPublishing();
|
|
367
|
+
|
|
368
|
+
// 断开
|
|
369
|
+
// await player.disconnect();
|
|
370
|
+
}
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
### Agora 接入
|
|
374
|
+
|
|
375
|
+
```typescript
|
|
376
|
+
import { AvatarPlayer, AgoraProvider } from '@spatialwalk/avatarkit-rtc';
|
|
377
|
+
import { AvatarView, Avatar } from '@spatialwalk/avatarkit';
|
|
378
|
+
|
|
379
|
+
async function initAgora() {
|
|
380
|
+
// 初始化 Avatar
|
|
381
|
+
const avatar = await Avatar.create('character-id');
|
|
382
|
+
const container = document.getElementById('avatar-container')!;
|
|
383
|
+
const avatarView = new AvatarView(avatar, container);
|
|
384
|
+
|
|
385
|
+
// 创建 Player
|
|
386
|
+
const provider = new AgoraProvider({ debugLogging: true });
|
|
387
|
+
const player = new AvatarPlayer(provider, avatarView);
|
|
388
|
+
|
|
389
|
+
// 连接
|
|
390
|
+
await player.connect({
|
|
391
|
+
appId: 'your-agora-app-id',
|
|
392
|
+
channel: 'my-channel',
|
|
393
|
+
token: 'your-token', // 生产环境必需
|
|
394
|
+
uid: 0, // 0 = 自动分配
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
// 开始说话
|
|
398
|
+
await player.startPublishing();
|
|
399
|
+
}
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
---
|
|
403
|
+
|
|
404
|
+
## Browser Compatibility
|
|
405
|
+
|
|
406
|
+
| Feature | Chrome | Firefox | Safari | Edge |
|
|
407
|
+
|---------|--------|---------|--------|------|
|
|
408
|
+
| LiveKit (VP8 + RTCRtpScriptTransform) | ✅ 94+ | ✅ 117+ | ❌ | ✅ 94+ |
|
|
409
|
+
| Agora (H.264 + SEI) | ✅ 74+ | ✅ 78+ | ✅ 14.1+ | ✅ 79+ |
|
|
410
|
+
|
|
411
|
+
**注意:** LiveKit 需要浏览器支持 `RTCRtpScriptTransform` API。
|
|
412
|
+
|
|
413
|
+
---
|
|
414
|
+
|
|
415
|
+
## License
|
|
416
|
+
|
|
417
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"animation-worker-CUXZycUw.js","sources":["../src/providers/livekit/animation-worker.ts"],"sourcesContent":["/**\n * RTCRtpScriptTransform Worker for Animation Track (VP8 Video)\n *\n * This worker handles extracting animation data from VP8 video frames.\n * The server prepends a minimal VP8 header to trick the browser's depacketizer.\n *\n * Frame format after depacketization (VP8 descriptor is stripped by depacketizer):\n * [0-9] VP8 frame header (10 bytes) - minimal keyframe structure, skipped\n * [10] flags (uint8) - packet flags\n * [11-14] msgLen (little-endian u32) - length of protobuf message\n * [15..15+msgLen) protobuf Message binary\n */\n\n// Type declarations for RTCRtpScriptTransform API (experimental)\ndeclare interface RTCTransformEvent extends Event {\n transformer: {\n readable: ReadableStream;\n writable: WritableStream;\n options?: unknown;\n };\n}\n\n// Worker global scope with RTCRtpScriptTransform support\ndeclare const self: DedicatedWorkerGlobalScope & {\n onrtctransform?: ((event: RTCTransformEvent) => void) | null;\n};\n\n// VP8 header size (frame tag + sync code + dimensions, descriptor already stripped)\nconst VP8_FRAME_HEADER_SIZE = 10;\n\n// Protocol constants (must match egress-server/metadata_track.go)\nconst METADATA_FIXED_HEADER_SIZE = 5; // 1B flags + 4B length\n\n// Packet flags\nconst PACKET_FLAG_IDLE = 0x01;\nconst PACKET_FLAG_START = 0x02;\nconst PACKET_FLAG_END = 0x04;\nconst PACKET_FLAG_GZIPPED = 0x08;\nconst PACKET_FLAG_TRANSITION = 0x10; // Transition packet - contains target frame for generating transition from idle\nconst PACKET_FLAG_TRANSITION_END = 0x20; // Transition end packet - contains last frame for generating transition back to idle\nconst PACKET_FLAG_REDUNDANT = 0x40; // Packet contains redundant previous frame for ALR loss recovery\n\ninterface FrameMetadata {\n flags: number;\n protobufLength: number;\n protobufData: Uint8Array;\n // ALR (Application-Level Redundancy) data\n hasRedundant: boolean;\n redundantLength: number;\n redundantData: Uint8Array | null;\n}\n\ninterface TransformOptions {\n operation: 'receiver';\n}\n\n// State for receiver\nlet receiverMetaCount = 0;\nlet lastLogTime = 0;\nlet totalFrames = 0;\nlet framesWithMeta = 0;\nlet wasIdle = false; // Track idle state for transition detection\n\n// ALR (Application-Level Redundancy) state for loss detection\nlet lastReceivedTimestamp: number | null = null;\nlet framesRecovered = 0;\nlet framesLost = 0;\n// Expected timestamp increment per animation frame (90000 Hz * 40ms = 3600)\n// Animation is sent every 2nd audio frame (40ms intervals)\nconst EXPECTED_TIMESTAMP_INCREMENT = 3600;\n\n// Sequence tracking for out-of-order detection\nlet lastRenderedSeq: number = -1; // Last sequence number sent to main thread\nlet framesOutOfOrder = 0; // Count of out-of-order frames detected\nlet framesDuplicate = 0; // Count of duplicate frames skipped\nlet framesDropped = 0; // Count of frames that were permanently lost (gaps in sequence)\nlet framesSent = 0; // Total frames successfully sent to main thread\n\nfunction parseMetadataHeader(data: Uint8Array): { meta: FrameMetadata; headerSize: number } | null {\n if (data.byteLength < METADATA_FIXED_HEADER_SIZE) return null;\n const view = new DataView(data.buffer, data.byteOffset, data.byteLength);\n const flags = data[0];\n const msgLen = view.getUint32(1, true); // little-endian\n\n // Calculate minimum size needed\n const headerSize = METADATA_FIXED_HEADER_SIZE + msgLen;\n if (data.byteLength < headerSize) return null;\n\n // The compressed payload (will be decompressed later)\n const compressedData = data.slice(METADATA_FIXED_HEADER_SIZE, METADATA_FIXED_HEADER_SIZE + msgLen);\n\n // Note: With the new ALR optimization, both frames are gzipped together\n // The format INSIDE the gzip (after decompression) is:\n // - ALR: [currentLen (4B)][currentData][prevLen (4B)][prevData]\n // - Non-ALR: just the raw protobuf data\n // We return the compressed data here; decompression and ALR parsing happens in receiverTransform\n\n const hasRedundant = (flags & PACKET_FLAG_REDUNDANT) !== 0;\n\n return {\n meta: {\n flags,\n protobufLength: msgLen,\n protobufData: compressedData, // This is still compressed at this point\n hasRedundant,\n redundantLength: 0, // Will be determined after decompression\n redundantData: null, // Will be extracted after decompression\n },\n headerSize,\n };\n}\n\nfunction isIdlePacket(flags: number): boolean {\n return (flags & PACKET_FLAG_IDLE) !== 0;\n}\n\nfunction isStartPacket(flags: number): boolean {\n return (flags & PACKET_FLAG_START) !== 0;\n}\n\nfunction isEndPacket(flags: number): boolean {\n return (flags & PACKET_FLAG_END) !== 0;\n}\n\nfunction isGzipped(flags: number): boolean {\n return (flags & PACKET_FLAG_GZIPPED) !== 0;\n}\n\nfunction isTransitionPacket(flags: number): boolean {\n return (flags & PACKET_FLAG_TRANSITION) !== 0;\n}\n\nfunction isTransitionEndPacket(flags: number): boolean {\n return (flags & PACKET_FLAG_TRANSITION_END) !== 0;\n}\n\n// Decompress gzipped data using DecompressionStream API\nasync function decompressGzip(data: Uint8Array): Promise<Uint8Array> {\n const ds = new DecompressionStream('gzip');\n const writer = ds.writable.getWriter();\n // Create a copy to ensure we have a plain ArrayBuffer, not SharedArrayBuffer\n const copy = new Uint8Array(data);\n writer.write(copy);\n writer.close();\n\n const reader = ds.readable.getReader();\n const chunks: Uint8Array[] = [];\n let totalLength = 0;\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n chunks.push(value);\n totalLength += value.length;\n }\n\n const result = new Uint8Array(totalLength);\n let offset = 0;\n for (const chunk of chunks) {\n result.set(chunk, offset);\n offset += chunk.length;\n }\n\n return result;\n}\n\n// Compression stats for logging\nlet totalCompressedBytes = 0;\nlet totalUncompressedBytes = 0;\n\n// ALR payload structure after decompression\ninterface ALRPayload {\n frameSeq: number; // Sequence number of current frame\n currentData: Uint8Array; // Current frame (N)\n prev1Data: Uint8Array | null; // Previous frame (N-1)\n prev2Data: Uint8Array | null; // Frame before previous (N-2)\n}\n\n// Parse ALR payload after decompression\n// ALR format: [frameSeq (4B)][currentLen (4B)][currentData][prev1Len (4B)][prev1Data][prev2Len (4B)][prev2Data]\n// Non-ALR format: [frameSeq (4B)][raw protobuf data]\n// Returns { frameSeq, currentData, prev1Data, prev2Data } or null if parsing fails\nfunction parseALRPayload(decompressed: Uint8Array, hasRedundant: boolean): ALRPayload | null {\n if (decompressed.byteLength < 4) return null;\n\n const view = new DataView(decompressed.buffer, decompressed.byteOffset, decompressed.byteLength);\n let offset = 0;\n\n // Parse frame sequence number\n const frameSeq = view.getUint32(offset, true);\n offset += 4;\n\n if (!hasRedundant) {\n // Non-ALR: rest is raw protobuf\n const currentData = decompressed.slice(offset);\n return { frameSeq, currentData, prev1Data: null, prev2Data: null };\n }\n\n // ALR format: parse current frame\n if (decompressed.byteLength < offset + 4) return null;\n const currentLen = view.getUint32(offset, true);\n offset += 4;\n if (decompressed.byteLength < offset + currentLen) return null;\n const currentData = decompressed.slice(offset, offset + currentLen);\n offset += currentLen;\n\n // Parse prev1 (N-1) if available\n let prev1Data: Uint8Array | null = null;\n if (decompressed.byteLength >= offset + 4) {\n const prev1Len = view.getUint32(offset, true);\n offset += 4;\n if (prev1Len > 0 && decompressed.byteLength >= offset + prev1Len) {\n prev1Data = decompressed.slice(offset, offset + prev1Len);\n offset += prev1Len;\n }\n }\n\n // Parse prev2 (N-2) if available\n let prev2Data: Uint8Array | null = null;\n if (decompressed.byteLength >= offset + 4) {\n const prev2Len = view.getUint32(offset, true);\n offset += 4;\n if (prev2Len > 0 && decompressed.byteLength >= offset + prev2Len) {\n prev2Data = decompressed.slice(offset, offset + prev2Len);\n }\n }\n\n return { frameSeq, currentData, prev1Data, prev2Data };\n}\n\n// Helper to send animation data to main thread with sequence tracking\n// Returns true if frame was sent, false if skipped (duplicate/out-of-order)\nfunction sendAnimationToMainThread(\n protobufData: Uint8Array,\n flags: number,\n frameSeq: number,\n isRecovered: boolean = false\n): boolean {\n const isIdle = isIdlePacket(flags);\n const isStart = isStartPacket(flags);\n const isEnd = isEndPacket(flags);\n\n // Check for out-of-order or duplicate frames\n if (frameSeq <= lastRenderedSeq && lastRenderedSeq !== -1 && !isStart) {\n if (frameSeq === lastRenderedSeq) {\n framesDuplicate++;\n } else {\n framesOutOfOrder++;\n }\n return false;\n }\n\n // Check for gap (frames we never received and couldn't recover) - these are DROPPED\n if (lastRenderedSeq !== -1 && frameSeq > lastRenderedSeq + 1 && !isStart) {\n const gap = frameSeq - lastRenderedSeq - 1;\n framesDropped += gap;\n }\n\n framesSent++;\n\n lastRenderedSeq = frameSeq;\n\n const protobufBuffer = new ArrayBuffer(protobufData.byteLength);\n new Uint8Array(protobufBuffer).set(protobufData);\n\n self.postMessage({\n type: 'animation',\n flags,\n isIdle,\n isStart,\n isEnd,\n isRecovered,\n frameSeq,\n protobufData: protobufBuffer,\n }, { transfer: [protobufBuffer] });\n\n return true;\n}\n\n// Receiver transform: extract animation data from video frames\n// Uses ALR (Application-Level Redundancy) to recover lost frames\nfunction receiverTransform(frame: RTCEncodedVideoFrame, _controller: TransformStreamDefaultController<RTCEncodedVideoFrame>) {\n totalFrames++;\n const data = new Uint8Array(frame.data);\n const currentTimestamp = frame.timestamp;\n\n // Skip the VP8 frame header (10 bytes) - the VP8 descriptor is already stripped by depacketizer\n // Check if we have enough data for VP8 header + at least 1 byte of animation data\n if (data.length <= VP8_FRAME_HEADER_SIZE) {\n return; // Frame too small, skip\n }\n\n // Extract animation data after VP8 header\n const animData = data.subarray(VP8_FRAME_HEADER_SIZE);\n\n const parsed = parseMetadataHeader(animData);\n\n if (parsed) {\n const { meta } = parsed;\n framesWithMeta++;\n receiverMetaCount++;\n\n const isIdle = isIdlePacket(meta.flags);\n const isStart = isStartPacket(meta.flags);\n const isEnd = isEndPacket(meta.flags);\n\n // ALR: Detect frame loss and recover using redundant data\n // Check if we missed frames by looking at timestamp gap\n if (lastReceivedTimestamp !== null && !isIdle && !isStart) {\n const timestampDelta = currentTimestamp - lastReceivedTimestamp;\n // Allow some tolerance (1.5x expected increment) for timing variations\n if (timestampDelta > EXPECTED_TIMESTAMP_INCREMENT * 1.5) {\n const missedFrames = Math.round(timestampDelta / EXPECTED_TIMESTAMP_INCREMENT) - 1;\n framesLost += missedFrames;\n\n // Try to recover using redundant data from current packet\n // With ALR optimization, up to 3 frames are gzipped together\n if (meta.hasRedundant && isGzipped(meta.flags)) {\n // Decompress to get all frames, then recover\n totalCompressedBytes += meta.protobufData.byteLength;\n\n decompressGzip(meta.protobufData)\n .then((decompressed) => {\n totalUncompressedBytes += decompressed.byteLength;\n\n const parsed = parseALRPayload(decompressed, true);\n if (parsed) {\n const currentSeq = parsed.frameSeq;\n\n // Determine how many frames we can recover based on how many were lost\n // and how many redundant frames we have\n const framesToRecover: Array<{data: Uint8Array, seq: number}> = [];\n\n if (missedFrames >= 2 && parsed.prev2Data) {\n framesToRecover.push({ data: parsed.prev2Data, seq: currentSeq - 2 });\n }\n if (missedFrames >= 1 && parsed.prev1Data) {\n framesToRecover.push({ data: parsed.prev1Data, seq: currentSeq - 1 });\n }\n\n const recovered = framesToRecover.length;\n if (recovered > 0) {\n framesRecovered += recovered;\n\n // Send recovered frames in chronological order (oldest first)\n for (const frame of framesToRecover) {\n sendAnimationToMainThread(frame.data, meta.flags & ~PACKET_FLAG_REDUNDANT, frame.seq, true);\n }\n }\n\n // Then send current frame\n sendAnimationToMainThread(parsed.currentData, meta.flags & ~PACKET_FLAG_REDUNDANT, currentSeq, false);\n }\n })\n .catch((err) => {\n console.error(`[Animation Worker] ALR decompression error:`, err);\n });\n\n // Update timestamp and return early (we've handled this frame)\n lastReceivedTimestamp = currentTimestamp;\n return;\n }\n }\n }\n\n // Update last received timestamp (skip for idle packets to avoid false loss detection)\n if (!isIdle) {\n lastReceivedTimestamp = currentTimestamp;\n }\n\n // Reset state on session start\n if (isStart) {\n lastReceivedTimestamp = currentTimestamp;\n framesRecovered = 0;\n framesLost = 0;\n lastRenderedSeq = -1;\n framesOutOfOrder = 0;\n framesDuplicate = 0;\n framesDropped = 0;\n framesSent = 0;\n }\n\n const isTransition = isTransitionPacket(meta.flags);\n\n if (isIdle) {\n // First idle packet - post idleStart event\n if (!wasIdle) {\n self.postMessage({ type: 'idleStart' });\n wasIdle = true;\n }\n } else if (isTransition && meta.protobufLength > 0) {\n // Transition packet - contains target frame for smooth transition from idle\n wasIdle = false;\n\n // Decompress if gzipped (transition packets are always gzipped)\n const gzipped = isGzipped(meta.flags);\n if (gzipped) {\n totalCompressedBytes += meta.protobufData.byteLength;\n\n decompressGzip(meta.protobufData).then((decompressed) => {\n totalUncompressedBytes += decompressed.byteLength;\n\n const protobufBuffer = new ArrayBuffer(decompressed.byteLength);\n new Uint8Array(protobufBuffer).set(decompressed);\n\n // Post transition event - main thread will generate transition frames\n self.postMessage({\n type: 'transition',\n flags: meta.flags,\n protobufData: protobufBuffer,\n }, { transfer: [protobufBuffer] });\n }).catch((err) => {\n console.error(`[Animation Worker] Gzip decompress error (transition):`, err);\n });\n } else {\n const protobufBuffer = new ArrayBuffer(meta.protobufData.byteLength);\n new Uint8Array(protobufBuffer).set(meta.protobufData);\n\n self.postMessage({\n type: 'transition',\n flags: meta.flags,\n protobufData: protobufBuffer,\n }, { transfer: [protobufBuffer] });\n }\n } else if (isTransitionEndPacket(meta.flags) && meta.protobufLength > 0) {\n // Transition end packet - contains last frame for smooth transition back to idle\n\n // Decompress if gzipped (transition end packets are always gzipped)\n const gzipped = isGzipped(meta.flags);\n if (gzipped) {\n totalCompressedBytes += meta.protobufData.byteLength;\n\n decompressGzip(meta.protobufData).then((decompressed) => {\n totalUncompressedBytes += decompressed.byteLength;\n\n const protobufBuffer = new ArrayBuffer(decompressed.byteLength);\n new Uint8Array(protobufBuffer).set(decompressed);\n\n // Post transition end event - main thread will generate reverse transition frames\n self.postMessage({\n type: 'transitionEnd',\n flags: meta.flags,\n protobufData: protobufBuffer,\n }, { transfer: [protobufBuffer] });\n }).catch((err) => {\n console.error(`[Animation Worker] Gzip decompress error (transitionEnd):`, err);\n });\n } else {\n const protobufBuffer = new ArrayBuffer(meta.protobufData.byteLength);\n new Uint8Array(protobufBuffer).set(meta.protobufData);\n\n self.postMessage({\n type: 'transitionEnd',\n flags: meta.flags,\n protobufData: protobufBuffer,\n }, { transfer: [protobufBuffer] });\n }\n } else if (meta.protobufLength > 0) {\n // Normal animation packet\n // Transition from idle to streaming (if not already transitioned via transition packet)\n if (wasIdle) {\n wasIdle = false;\n }\n\n // Decompress if gzipped\n const gzipped = isGzipped(meta.flags);\n if (gzipped) {\n // Track compression stats\n totalCompressedBytes += meta.protobufData.byteLength;\n\n // Async decompression\n decompressGzip(meta.protobufData).then((decompressed) => {\n totalUncompressedBytes += decompressed.byteLength;\n\n // Parse ALR format to get frame sequence and data\n // Format: [frameSeq (4B)][currentLen (4B)][currentData]... or [frameSeq (4B)][raw protobuf]\n const parsed = parseALRPayload(decompressed, meta.hasRedundant);\n if (parsed) {\n sendAnimationToMainThread(parsed.currentData, meta.flags & ~PACKET_FLAG_REDUNDANT, parsed.frameSeq, false);\n }\n }).catch(() => {});\n } else {\n // Uncompressed - this shouldn't happen in normal operation but handle it\n const protobufBuffer = new ArrayBuffer(meta.protobufData.byteLength);\n new Uint8Array(protobufBuffer).set(meta.protobufData);\n\n self.postMessage({\n type: 'animation',\n flags: meta.flags,\n isIdle: false,\n isStart,\n isEnd,\n frameSeq: -1, // Unknown sequence\n protobufData: protobufBuffer,\n }, { transfer: [protobufBuffer] });\n }\n }\n\n // Report stats every second\n const now = performance.now();\n if (now - lastLogTime > 1000) {\n lastLogTime = now;\n self.postMessage({\n type: 'metadata',\n protobufLength: meta.protobufLength,\n framesPerSec: receiverMetaCount,\n totalFrames,\n framesWithMeta,\n framesLost,\n framesRecovered,\n framesOutOfOrder,\n framesDuplicate,\n framesDropped,\n framesSent,\n lastRenderedSeq,\n });\n receiverMetaCount = 0;\n }\n }\n\n // For video, we don't enqueue the frame since it's not real video\n // The browser's VP8 decoder would fail on our custom data anyway\n // controller.enqueue(frame);\n}\n\n// Handle rtctransform event (RTCRtpScriptTransform)\nself.onrtctransform = (event: RTCTransformEvent) => {\n const transformer = event.transformer;\n const options = transformer.options as TransformOptions;\n\n try {\n if (options.operation === 'receiver') {\n // Reset state\n receiverMetaCount = 0;\n lastLogTime = 0;\n totalFrames = 0;\n framesWithMeta = 0;\n wasIdle = false;\n lastReceivedTimestamp = null;\n framesRecovered = 0;\n framesLost = 0;\n lastRenderedSeq = -1;\n framesOutOfOrder = 0;\n framesDuplicate = 0;\n framesDropped = 0;\n framesSent = 0;\n\n transformer.readable\n .pipeThrough(new TransformStream({ transform: receiverTransform }))\n .pipeTo(transformer.writable)\n .catch((err: unknown) => {\n console.error('[Animation Worker] Pipeline error:', err);\n self.postMessage({ type: 'error', error: `Animation receiver pipe error: ${err}` });\n });\n\n self.postMessage({ type: 'ready', operation: 'receiver' });\n }\n } catch (err) {\n console.error('[Animation Worker] Setup error:', err);\n self.postMessage({ type: 'error', error: `Animation transform setup error: ${err}` });\n }\n};\n\n// Handle message-based initialization for fallback\nself.onmessage = (event: MessageEvent) => {\n const { type } = event.data;\n if (type === 'init') {\n self.postMessage({ type: 'initialized' });\n }\n};\n\nexport {};\n"],"names":["currentData","parsed","frame"],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Animation Handler - Orchestrates animation playback and transitions.
|
|
3
|
+
*
|
|
4
|
+
* This module handles:
|
|
5
|
+
* - Animation frame rendering
|
|
6
|
+
* - Transition playback from idle to animation and back
|
|
7
|
+
* - Frame timing at 25fps
|
|
8
|
+
* - Session state tracking
|
|
9
|
+
*
|
|
10
|
+
* The handler relies on server-sent packet flags (Transition, TransitionEnd, Idle)
|
|
11
|
+
* to determine when to generate transitions, rather than maintaining complex internal state.
|
|
12
|
+
*
|
|
13
|
+
* @internal
|
|
14
|
+
* @packageDocumentation
|
|
15
|
+
*/
|
|
16
|
+
export {};
|
|
17
|
+
//# sourceMappingURL=AnimationHandler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AnimationHandler.d.ts","sourceRoot":"","sources":["../../src/core/AnimationHandler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG"}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { RTCProvider } from './RTCProvider';
|
|
2
|
+
import { RTCConnectionConfig } from '../types';
|
|
3
|
+
import { RTCProviderEvents } from './types';
|
|
4
|
+
import { LogLevel } from '../utils';
|
|
5
|
+
import { AvatarView } from '@spatialwalk/avatarkit';
|
|
6
|
+
/**
|
|
7
|
+
* Options for configuring AvatarPlayer.
|
|
8
|
+
*/
|
|
9
|
+
export interface AvatarPlayerOptions {
|
|
10
|
+
/**
|
|
11
|
+
* Log level for SDK output.
|
|
12
|
+
* - 'info': Show all logs (debug mode)
|
|
13
|
+
* - 'warning': Show warnings and errors (default)
|
|
14
|
+
* - 'error': Show only errors
|
|
15
|
+
* - 'none': Disable all logs
|
|
16
|
+
*/
|
|
17
|
+
logLevel?: LogLevel;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Unified Avatar Player.
|
|
21
|
+
*
|
|
22
|
+
* Main entry point for applications to interact with RTC avatar streaming.
|
|
23
|
+
* Handles connection management, audio publishing, animation rendering,
|
|
24
|
+
* and coordinates with avatarkit for display.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```typescript
|
|
28
|
+
* import { AvatarPlayer, LiveKitProvider } from '@spatialwalk/avatarkit-rtc';
|
|
29
|
+
* import { AvatarView, Avatar } from '@spatialwalk/avatarkit';
|
|
30
|
+
*
|
|
31
|
+
* // Create avatar and view (from avatarkit)
|
|
32
|
+
* const avatar = await Avatar.create(characterId);
|
|
33
|
+
* const avatarView = new AvatarView(avatar, container);
|
|
34
|
+
*
|
|
35
|
+
* // Create player
|
|
36
|
+
* const provider = new LiveKitProvider();
|
|
37
|
+
* const player = new AvatarPlayer(provider, avatarView);
|
|
38
|
+
*
|
|
39
|
+
* // Connect and start
|
|
40
|
+
* await player.connect({ url: 'wss://...', token: '...' });
|
|
41
|
+
* await player.startPublishing(); // Start microphone
|
|
42
|
+
*
|
|
43
|
+
* // Disconnect when done
|
|
44
|
+
* await player.disconnect();
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export declare class AvatarPlayer {
|
|
48
|
+
/**
|
|
49
|
+
* Create a new AvatarPlayer instance.
|
|
50
|
+
* @param provider - RTC provider implementation (e.g., LiveKitProvider)
|
|
51
|
+
* @param avatarView - AvatarView instance from @spatialwalk/avatarkit
|
|
52
|
+
* @param options - Optional configuration for transitions
|
|
53
|
+
*/
|
|
54
|
+
constructor(provider: RTCProvider, avatarView: AvatarView, options?: AvatarPlayerOptions);
|
|
55
|
+
/**
|
|
56
|
+
* Check if currently connected to RTC server.
|
|
57
|
+
*/
|
|
58
|
+
get isConnected(): boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Connect to RTC server.
|
|
61
|
+
* @param config - Connection configuration (URL, token, room name, etc.)
|
|
62
|
+
* @throws Error if already connected or connection fails
|
|
63
|
+
*/
|
|
64
|
+
connect(config: RTCConnectionConfig): Promise<void>;
|
|
65
|
+
/**
|
|
66
|
+
* Disconnect from RTC server.
|
|
67
|
+
* Stops all tracks and cleans up resources.
|
|
68
|
+
*/
|
|
69
|
+
disconnect(): Promise<void>;
|
|
70
|
+
/**
|
|
71
|
+
* Start publishing microphone audio to the RTC server.
|
|
72
|
+
* Requests microphone permission and starts publishing.
|
|
73
|
+
* @throws Error if permission denied or no microphone found
|
|
74
|
+
*/
|
|
75
|
+
startPublishing(): Promise<void>;
|
|
76
|
+
/**
|
|
77
|
+
* Stop publishing microphone audio.
|
|
78
|
+
*/
|
|
79
|
+
stopPublishing(): Promise<void>;
|
|
80
|
+
/**
|
|
81
|
+
* Get current connection state.
|
|
82
|
+
* @returns Connection state string (e.g., 'connected', 'disconnected')
|
|
83
|
+
*/
|
|
84
|
+
getConnectionState(): string;
|
|
85
|
+
/**
|
|
86
|
+
* Get the native RTC client object.
|
|
87
|
+
*
|
|
88
|
+
* Returns the underlying RTC client for advanced use cases:
|
|
89
|
+
* - LiveKit: Returns the Room instance
|
|
90
|
+
* - Agora: Returns the IAgoraRTCClient instance
|
|
91
|
+
*
|
|
92
|
+
* @returns The native RTC client object, or null if not connected
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* ```typescript
|
|
96
|
+
* // Access LiveKit Room
|
|
97
|
+
* const room = player.getNativeClient() as Room;
|
|
98
|
+
* console.log('Participants:', room?.remoteParticipants.size);
|
|
99
|
+
*
|
|
100
|
+
* // Access Agora Client
|
|
101
|
+
* const client = player.getNativeClient() as IAgoraRTCClient;
|
|
102
|
+
* console.log('State:', client?.connectionState);
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
getNativeClient(): unknown;
|
|
106
|
+
/**
|
|
107
|
+
* Add event listener.
|
|
108
|
+
* @param event - Event name ('connected', 'disconnected', 'error', etc.)
|
|
109
|
+
* @param handler - Event handler function
|
|
110
|
+
*/
|
|
111
|
+
on<K extends keyof RTCProviderEvents>(event: K, handler: RTCProviderEvents[K]): void;
|
|
112
|
+
/**
|
|
113
|
+
* Remove event listener.
|
|
114
|
+
* @param event - Event name
|
|
115
|
+
* @param handler - Event handler function (must be same reference as added)
|
|
116
|
+
*/
|
|
117
|
+
off<K extends keyof RTCProviderEvents>(event: K, handler: RTCProviderEvents[K]): void;
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=AvatarPlayer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AvatarPlayer.d.ts","sourceRoot":"","sources":["../../src/core/AvatarPlayer.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AAEpD,OAAO,KAAK,EAA2B,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAC1E,OAAO,EAAmB,KAAK,QAAQ,EAAE,MAAM,UAAU,CAAC;AAG1D,OAAO,KAAK,EAAE,UAAU,EAAgB,MAAM,wBAAwB,CAAC;AAEvE;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,QAAQ,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,qBAAa,YAAY;IAmBvB;;;;;OAKG;gBAED,QAAQ,EAAE,WAAW,EACrB,UAAU,EAAE,UAAU,EACtB,OAAO,CAAC,EAAE,mBAAmB;IAmB/B;;OAEG;IACH,IAAI,WAAW,IAAI,OAAO,CAEzB;IAED;;;;OAIG;IACG,OAAO,CAAC,MAAM,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;IAWzD;;;OAGG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAejC;;;;OAIG;IACG,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC;IAYtC;;OAEG;IACG,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;IAiBrC;;;OAGG;IACH,kBAAkB,IAAI,MAAM;IAI5B;;;;;;;;;;;;;;;;;;;OAmBG;IACH,eAAe,IAAI,OAAO;IAI1B;;;;OAIG;IACH,EAAE,CAAC,CAAC,SAAS,MAAM,iBAAiB,EAClC,KAAK,EAAE,CAAC,EACR,OAAO,EAAE,iBAAiB,CAAC,CAAC,CAAC,GAC5B,IAAI;IAIP;;;;OAIG;IACH,GAAG,CAAC,CAAC,SAAS,MAAM,iBAAiB,EACnC,KAAK,EAAE,CAAC,EACR,OAAO,EAAE,iBAAiB,CAAC,CAAC,CAAC,GAC5B,IAAI;CA+FR"}
|