@spatialwalk/avatarkit 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.
Files changed (99) hide show
  1. package/README.md +375 -0
  2. package/dist/StreamingAudioPlayer-C2TfYsO8.js +293 -0
  3. package/dist/StreamingAudioPlayer-C2TfYsO8.js.map +1 -0
  4. package/dist/animation/AnimationWebSocketClient.d.ts +50 -0
  5. package/dist/animation/AnimationWebSocketClient.d.ts.map +1 -0
  6. package/dist/animation/utils/eventEmitter.d.ts +13 -0
  7. package/dist/animation/utils/eventEmitter.d.ts.map +1 -0
  8. package/dist/animation/utils/flameConverter.d.ts +26 -0
  9. package/dist/animation/utils/flameConverter.d.ts.map +1 -0
  10. package/dist/audio/AnimationPlayer.d.ts +53 -0
  11. package/dist/audio/AnimationPlayer.d.ts.map +1 -0
  12. package/dist/audio/StreamingAudioPlayer.d.ts +113 -0
  13. package/dist/audio/StreamingAudioPlayer.d.ts.map +1 -0
  14. package/dist/avatar_core_wasm-DmkU6dYn.js +1666 -0
  15. package/dist/avatar_core_wasm-DmkU6dYn.js.map +1 -0
  16. package/dist/avatar_core_wasm.wasm +0 -0
  17. package/dist/config/app-config.d.ts +48 -0
  18. package/dist/config/app-config.d.ts.map +1 -0
  19. package/dist/config/constants.d.ts +13 -0
  20. package/dist/config/constants.d.ts.map +1 -0
  21. package/dist/config/region-config.d.ts +17 -0
  22. package/dist/config/region-config.d.ts.map +1 -0
  23. package/dist/config/sdk-config-loader.d.ts +12 -0
  24. package/dist/config/sdk-config-loader.d.ts.map +1 -0
  25. package/dist/core/Avatar.d.ts +23 -0
  26. package/dist/core/Avatar.d.ts.map +1 -0
  27. package/dist/core/AvatarController.d.ts +125 -0
  28. package/dist/core/AvatarController.d.ts.map +1 -0
  29. package/dist/core/AvatarDownloader.d.ts +100 -0
  30. package/dist/core/AvatarDownloader.d.ts.map +1 -0
  31. package/dist/core/AvatarKit.d.ts +60 -0
  32. package/dist/core/AvatarKit.d.ts.map +1 -0
  33. package/dist/core/AvatarManager.d.ts +26 -0
  34. package/dist/core/AvatarManager.d.ts.map +1 -0
  35. package/dist/core/AvatarView.d.ts +158 -0
  36. package/dist/core/AvatarView.d.ts.map +1 -0
  37. package/dist/generated/driveningress/v1/driveningress.d.ts +80 -0
  38. package/dist/generated/driveningress/v1/driveningress.d.ts.map +1 -0
  39. package/dist/generated/driveningress/v2/driveningress.d.ts +81 -0
  40. package/dist/generated/driveningress/v2/driveningress.d.ts.map +1 -0
  41. package/dist/generated/google/protobuf/any.d.ts +145 -0
  42. package/dist/generated/google/protobuf/any.d.ts.map +1 -0
  43. package/dist/generated/google/protobuf/struct.d.ts +108 -0
  44. package/dist/generated/google/protobuf/struct.d.ts.map +1 -0
  45. package/dist/generated/google/protobuf/timestamp.d.ts +129 -0
  46. package/dist/generated/google/protobuf/timestamp.d.ts.map +1 -0
  47. package/dist/generated/jsonapi/v1/base.d.ts +140 -0
  48. package/dist/generated/jsonapi/v1/base.d.ts.map +1 -0
  49. package/dist/generated/platform/v1/asset_groups.d.ts +225 -0
  50. package/dist/generated/platform/v1/asset_groups.d.ts.map +1 -0
  51. package/dist/generated/platform/v1/assets.d.ts +149 -0
  52. package/dist/generated/platform/v1/assets.d.ts.map +1 -0
  53. package/dist/generated/platform/v1/character.d.ts +395 -0
  54. package/dist/generated/platform/v1/character.d.ts.map +1 -0
  55. package/dist/generated/platform/v1/redeem.d.ts +22 -0
  56. package/dist/generated/platform/v1/redeem.d.ts.map +1 -0
  57. package/dist/index-DwhR9l52.js +9712 -0
  58. package/dist/index-DwhR9l52.js.map +1 -0
  59. package/dist/index.d.ts +12 -0
  60. package/dist/index.d.ts.map +1 -0
  61. package/dist/index.js +19 -0
  62. package/dist/index.js.map +1 -0
  63. package/dist/renderer/RenderSystem.d.ts +77 -0
  64. package/dist/renderer/RenderSystem.d.ts.map +1 -0
  65. package/dist/renderer/covariance.d.ts +13 -0
  66. package/dist/renderer/covariance.d.ts.map +1 -0
  67. package/dist/renderer/renderer.d.ts +8 -0
  68. package/dist/renderer/renderer.d.ts.map +1 -0
  69. package/dist/renderer/sortSplats.d.ts +12 -0
  70. package/dist/renderer/sortSplats.d.ts.map +1 -0
  71. package/dist/renderer/webgl/reorderData.d.ts +14 -0
  72. package/dist/renderer/webgl/reorderData.d.ts.map +1 -0
  73. package/dist/renderer/webgl/webglRenderer.d.ts +66 -0
  74. package/dist/renderer/webgl/webglRenderer.d.ts.map +1 -0
  75. package/dist/renderer/webgpu/webgpuRenderer.d.ts +54 -0
  76. package/dist/renderer/webgpu/webgpuRenderer.d.ts.map +1 -0
  77. package/dist/types/character-settings.d.ts +23 -0
  78. package/dist/types/character-settings.d.ts.map +1 -0
  79. package/dist/types/character.d.ts +39 -0
  80. package/dist/types/character.d.ts.map +1 -0
  81. package/dist/types/index.d.ts +68 -0
  82. package/dist/types/index.d.ts.map +1 -0
  83. package/dist/utils/animation-interpolation.d.ts +17 -0
  84. package/dist/utils/animation-interpolation.d.ts.map +1 -0
  85. package/dist/utils/error-utils.d.ts +27 -0
  86. package/dist/utils/error-utils.d.ts.map +1 -0
  87. package/dist/utils/logger.d.ts +35 -0
  88. package/dist/utils/logger.d.ts.map +1 -0
  89. package/dist/utils/posthog-tracker.d.ts +82 -0
  90. package/dist/utils/posthog-tracker.d.ts.map +1 -0
  91. package/dist/utils/reqId.d.ts +20 -0
  92. package/dist/utils/reqId.d.ts.map +1 -0
  93. package/dist/utils/toast.d.ts +74 -0
  94. package/dist/utils/toast.d.ts.map +1 -0
  95. package/dist/wasm/avatarCoreAdapter.d.ts +188 -0
  96. package/dist/wasm/avatarCoreAdapter.d.ts.map +1 -0
  97. package/dist/wasm/avatarCoreMemory.d.ts +141 -0
  98. package/dist/wasm/avatarCoreMemory.d.ts.map +1 -0
  99. package/package.json +62 -0
package/README.md ADDED
@@ -0,0 +1,375 @@
1
+ # SPAvatarKit SDK
2
+
3
+ 基于 3D Gaussian Splatting 的实时虚拟人物头像渲染 SDK,支持音频驱动的动画渲染和高质量 3D 渲染。
4
+
5
+ ## 🚀 特性
6
+
7
+ - **3D Gaussian Splatting 渲染** - 基于最新的点云渲染技术,提供高质量的 3D 虚拟人物
8
+ - **音频驱动的实时动画渲染** - 用户提供音频数据,SDK 负责接收动画数据并渲染
9
+ - **WebGPU/WebGL 双渲染后端** - 自动选择最佳渲染后端,确保兼容性
10
+ - **WASM 高性能计算** - 使用 C++ 编译的 WebAssembly 模块进行几何计算
11
+ - **TypeScript 支持** - 完整的类型定义和智能提示
12
+ - **模块化架构** - 清晰的组件分离,易于集成和扩展
13
+
14
+ ## 📦 安装
15
+
16
+ ```bash
17
+ npm install @spatialwalk/avatarkit
18
+ ```
19
+
20
+ ## 🎯 快速开始
21
+
22
+ ### 基础使用
23
+
24
+ ```typescript
25
+ import {
26
+ AvatarKit,
27
+ AvatarManager,
28
+ AvatarView,
29
+ Configuration,
30
+ Environment
31
+ } from '@spatialwalk/avatarkit'
32
+
33
+ // 1. 初始化 SDK
34
+ const configuration: Configuration = {
35
+ environment: Environment.test,
36
+ }
37
+
38
+ await AvatarKit.initialize('your-app-id', configuration)
39
+
40
+ // 设置 sessionToken(如果需要,单独调用)
41
+ // AvatarKit.setSessionToken('your-session-token')
42
+
43
+ // 2. 加载角色
44
+ const avatarManager = new AvatarManager()
45
+ const avatar = await avatarManager.load('character-id', (progress) => {
46
+ console.log(`Loading progress: ${progress.progress}%`)
47
+ })
48
+
49
+ // 3. 创建视图(自动创建 Canvas 和 AvatarController)
50
+ const container = document.getElementById('avatar-container')
51
+ const avatarView = new AvatarView(avatar, container)
52
+
53
+ // 4. 启动实时通信
54
+ await avatarView.avatarController.start()
55
+
56
+ // 5. 发送音频数据
57
+ // 如果音频是 Uint8Array,可以使用 slice().buffer 转换为 ArrayBuffer
58
+ const audioUint8 = new Uint8Array(1024) // 示例:音频数据
59
+ const audioData = audioUint8.slice().buffer // 简化的转换方式,适用于 ArrayBuffer 和 SharedArrayBuffer
60
+ avatarView.avatarController.send(audioData, false) // 发送音频数据,积累到一定量后会自动开始播放
61
+ avatarView.avatarController.send(audioData, true) // end=true 表示立即返回动画数据,不再积累
62
+ ```
63
+
64
+ ### 完整示例
65
+
66
+ 查看 GitHub 仓库中的示例代码了解完整的使用流程。
67
+
68
+ ## 🏗️ 架构概览
69
+
70
+ ### 核心组件
71
+
72
+ - **AvatarKit** - SDK 初始化和管理
73
+ - **AvatarManager** - 角色资源加载和管理
74
+ - **AvatarView** - 3D 渲染视图(内部包含 AvatarController)
75
+ - **AvatarController** - 实时通信和数据处理
76
+ - **AvatarCoreAdapter** - WASM 模块适配器
77
+
78
+ ### 数据流
79
+
80
+ ```
81
+ 用户音频输入(16kHz mono PCM) → AvatarController → WebSocket → 后台处理
82
+
83
+ 后台返回动画数据(FLAME 关键帧) → AvatarController → AnimationPlayer
84
+
85
+ FLAME 参数 → AvatarCore.computeFrameFlatFromParams() → Splat 数据
86
+
87
+ Splat 数据 → RenderSystem → WebGPU/WebGL → Canvas 渲染
88
+ ```
89
+
90
+ **注意:** 用户需要自己提供音频数据(16kHz mono PCM),SDK 负责接收动画数据并渲染。
91
+
92
+ ## 📚 API 参考
93
+
94
+ ### AvatarKit
95
+
96
+ SDK 的核心管理类,负责初始化和全局配置。
97
+
98
+ ```typescript
99
+ // 初始化 SDK
100
+ await AvatarKit.initialize(appId: string, configuration: Configuration)
101
+
102
+ // 检查初始化状态
103
+ const isInitialized = AvatarKit.isInitialized
104
+
105
+ // 清理资源(不再使用时必须调用)
106
+ AvatarKit.cleanup()
107
+ ```
108
+
109
+ ### AvatarManager
110
+
111
+ 角色资源管理器,负责下载、缓存和加载角色数据。
112
+
113
+ ```typescript
114
+ const manager = new AvatarManager()
115
+
116
+ // 加载角色
117
+ const avatar = await manager.load(
118
+ characterId: string,
119
+ onProgress?: (progress: LoadProgressInfo) => void
120
+ )
121
+
122
+ // 清理缓存
123
+ manager.clearCache()
124
+ ```
125
+
126
+ ### AvatarView
127
+
128
+ 3D 渲染视图,内部自动创建和管理 AvatarController。
129
+
130
+ ```typescript
131
+ // 创建视图(Canvas 会自动添加到容器中)
132
+ const avatarView = new AvatarView(avatar: Avatar, container?: HTMLElement)
133
+
134
+ // 获取 Canvas 元素
135
+ const canvas = avatarView.getCanvas()
136
+
137
+ // 设置背景
138
+ avatarView.setBackgroundImage('path/to/image.jpg')
139
+ avatarView.setBackgroundOpaque(true)
140
+
141
+ // 更新相机配置
142
+ avatarView.updateCameraConfig(cameraConfig: CameraConfig)
143
+
144
+ // 清理资源
145
+ avatarView.dispose()
146
+ ```
147
+
148
+ ### AvatarController
149
+
150
+ 实时通信控制器,处理 WebSocket 连接和动画数据。
151
+
152
+ ```typescript
153
+ // 启动连接
154
+ await avatarView.avatarController.start()
155
+
156
+ // 发送音频数据
157
+ avatarView.avatarController.send(audioData: ArrayBuffer, end: boolean)
158
+ // audioData: 音频数据(ArrayBuffer 格式)
159
+ // end: false(默认)- 正常发送音频数据,服务端会积累音频数据,积累到一定量后会自动返回动画数据并开始同步播放动画和音频
160
+ // end: true - 立即返回动画数据,不再积累,用于结束当前对话或需要立即响应的场景
161
+
162
+ // 打断对话
163
+ avatarView.avatarController.interrupt()
164
+
165
+ // 关闭连接
166
+ avatarView.avatarController.close()
167
+
168
+ // 设置事件回调
169
+ avatarView.avatarController.onConnectionState = (state: ConnectionState) => {}
170
+ avatarView.avatarController.onAvatarState = (state: AvatarState) => {}
171
+ avatarView.avatarController.onError = (error: Error) => {}
172
+
173
+ // 注意:不支持 sendText() 方法,调用会抛出错误
174
+ ```
175
+
176
+ ## 🔧 配置
177
+
178
+ ### Configuration
179
+
180
+ ```typescript
181
+ interface Configuration {
182
+ environment: Environment
183
+ }
184
+ ```
185
+
186
+ **说明:**
187
+ - `environment`: 指定环境(cn/us/test),SDK 会根据环境自动使用对应的 API 地址和 WebSocket 地址
188
+ - `sessionToken`: 通过 `AvatarKit.setSessionToken()` 单独设置,而不是在 Configuration 中
189
+
190
+ enum Environment {
191
+ cn = 'cn', // 中国区
192
+ us = 'us', // 美国区
193
+ test = 'test' // 测试环境
194
+ }
195
+ ```
196
+
197
+ ### CameraConfig
198
+
199
+ ```typescript
200
+ interface CameraConfig {
201
+ position: [number, number, number] // 相机位置
202
+ target: [number, number, number] // 相机目标
203
+ fov: number // 视野角度
204
+ near: number // 近裁剪面
205
+ far: number // 远裁剪面
206
+ up?: [number, number, number] // 上方向
207
+ aspect?: number // 宽高比
208
+ }
209
+ ```
210
+
211
+ ## 📊 状态管理
212
+
213
+ ### ConnectionState
214
+
215
+ ```typescript
216
+ enum ConnectionState {
217
+ disconnected = 'disconnected',
218
+ connecting = 'connecting',
219
+ connected = 'connected',
220
+ failed = 'failed'
221
+ }
222
+ ```
223
+
224
+ ### AvatarState
225
+
226
+ ```typescript
227
+ enum AvatarState {
228
+ idle = 'idle', // 空闲状态,呈现呼吸态
229
+ active = 'active', // 活跃中,等待可播放内容
230
+ playing = 'playing' // 播放中
231
+ }
232
+ ```
233
+
234
+ ## 🎨 渲染系统
235
+
236
+ SDK 支持两种渲染后端:
237
+
238
+ - **WebGPU** - 现代浏览器的高性能渲染
239
+ - **WebGL** - 兼容性更好的传统渲染
240
+
241
+ 渲染系统会自动选择最佳的后端,无需手动配置。
242
+
243
+ ## 🔍 调试和监控
244
+
245
+ ### 日志系统
246
+
247
+ SDK 内置了完整的日志系统,支持不同级别的日志输出:
248
+
249
+ ```typescript
250
+ import { logger } from '@spatialwalk/avatarkit'
251
+
252
+ // 设置日志级别
253
+ logger.setLevel('verbose') // 'basic' | 'verbose'
254
+
255
+ // 手动日志输出
256
+ logger.log('Info message')
257
+ logger.warn('Warning message')
258
+ logger.error('Error message')
259
+ ```
260
+
261
+ ### 性能监控
262
+
263
+ SDK 提供了性能监控接口,可以监控渲染性能:
264
+
265
+ ```typescript
266
+ // 获取渲染性能统计
267
+ const stats = avatarView.getPerformanceStats()
268
+
269
+ if (stats) {
270
+ console.log(`渲染耗时: ${stats.renderTime.toFixed(2)}ms`)
271
+ console.log(`排序耗时: ${stats.sortTime.toFixed(2)}ms`)
272
+ console.log(`渲染后端: ${stats.backend}`)
273
+
274
+ // 计算帧率
275
+ const fps = 1000 / stats.renderTime
276
+ console.log(`帧率: ${fps.toFixed(2)} FPS`)
277
+ }
278
+
279
+ // 定期监控性能
280
+ setInterval(() => {
281
+ const stats = avatarView.getPerformanceStats()
282
+ if (stats) {
283
+ // 发送到监控服务或显示在 UI 上
284
+ console.log('Performance:', stats)
285
+ }
286
+ }, 1000)
287
+ ```
288
+
289
+ **性能统计说明**:
290
+ - `renderTime`: 总渲染耗时(毫秒),包含排序和 GPU 渲染
291
+ - `sortTime`: 排序耗时(毫秒),使用 Radix Sort 算法对点云进行深度排序
292
+ - `backend`: 当前使用的渲染后端(`'webgpu'` | `'webgl'` | `null`)
293
+
294
+ ## 🚨 错误处理
295
+
296
+ ### SPAvatarError
297
+
298
+ SDK 使用自定义错误类型,提供更详细的错误信息:
299
+
300
+ ```typescript
301
+ import { SPAvatarError } from '@spatialwalk/avatarkit'
302
+
303
+ try {
304
+ await avatarView.avatarController.start()
305
+ } catch (error) {
306
+ if (error instanceof SPAvatarError) {
307
+ console.error('SDK Error:', error.message, error.code)
308
+ } else {
309
+ console.error('Unknown error:', error)
310
+ }
311
+ }
312
+ ```
313
+
314
+ ### 错误回调
315
+
316
+ ```typescript
317
+ avatarView.avatarController.onError = (error: Error) => {
318
+ console.error('AvatarController error:', error)
319
+ // 处理错误,比如重连、用户提示等
320
+ }
321
+ ```
322
+
323
+ ## 🔄 资源管理
324
+
325
+ ### 生命周期管理
326
+
327
+ ```typescript
328
+ // 初始化
329
+ const avatarView = new AvatarView(avatar, container)
330
+ await avatarView.avatarController.start()
331
+
332
+ // 使用
333
+ avatarView.avatarController.send(audioData, false)
334
+
335
+ // 清理
336
+ avatarView.dispose() // 自动清理所有资源
337
+ ```
338
+
339
+ ### 内存优化
340
+
341
+ - SDK 自动管理 WASM 内存分配
342
+ - 支持角色和动画资源的动态加载/卸载
343
+ - 提供内存使用监控接口
344
+
345
+ ### 音频数据发送
346
+
347
+ `send()` 方法接收 `ArrayBuffer` 格式的音频数据:
348
+
349
+ **使用说明:**
350
+ - `audioData`: 音频数据(ArrayBuffer 格式)
351
+ - `end=false`(默认)- 正常发送音频数据,服务端会积累音频数据,积累到一定量后会自动返回动画数据并开始同步播放动画和音频
352
+ - `end=true` - 立即返回动画数据,不再积累,用于结束当前对话或需要立即响应的场景
353
+ - **重要**:不需要等待 `end=true` 才开始播放,积累到一定音频数据后就会自动开始播放
354
+
355
+ ## 🌐 浏览器兼容性
356
+
357
+ - **Chrome/Edge** 90+ (推荐 WebGPU)
358
+ - **Firefox** 90+ (WebGL)
359
+ - **Safari** 14+ (WebGL)
360
+ - **移动端** iOS 14+, Android 8+
361
+
362
+ ## 📝 许可证
363
+
364
+ MIT License
365
+
366
+ ## 🤝 贡献
367
+
368
+ 欢迎提交 Issue 和 Pull Request!
369
+
370
+ ## 📞 支持
371
+
372
+ 如有问题,请联系:
373
+ - 邮箱:support@spavatar.com
374
+ - 文档:https://docs.spavatar.com
375
+ - GitHub:https://github.com/spavatar/sdk
@@ -0,0 +1,293 @@
1
+ var c = Object.defineProperty;
2
+ var g = (h, t, e) => t in h ? c(h, t, { enumerable: !0, configurable: !0, writable: !0, value: e }) : h[t] = e;
3
+ var s = (h, t, e) => g(h, typeof t != "symbol" ? t + "" : t, e);
4
+ import { l as r } from "./index-DwhR9l52.js";
5
+ class k {
6
+ constructor(t) {
7
+ // AudioContext is managed internally
8
+ s(this, "audioContext", null);
9
+ s(this, "sampleRate");
10
+ s(this, "channelCount");
11
+ s(this, "debug");
12
+ // Session-level state
13
+ s(this, "sessionId");
14
+ s(this, "sessionStartTime", 0);
15
+ // AudioContext time when session started
16
+ s(this, "pausedTimeOffset", 0);
17
+ // Accumulated paused time
18
+ s(this, "pausedAt", 0);
19
+ // Time when paused
20
+ s(this, "scheduledTime", 0);
21
+ // Next chunk schedule time in AudioContext time
22
+ // Playback state
23
+ s(this, "isPlaying", !1);
24
+ s(this, "isPaused", !1);
25
+ // Audio buffer queue
26
+ s(this, "audioChunks", []);
27
+ s(this, "scheduledChunks", 0);
28
+ // Number of chunks already scheduled
29
+ s(this, "activeSources", []);
30
+ // Event callbacks
31
+ s(this, "onEndedCallback");
32
+ this.sessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, this.sampleRate = (t == null ? void 0 : t.sampleRate) ?? 24e3, this.channelCount = (t == null ? void 0 : t.channelCount) ?? 1, this.debug = (t == null ? void 0 : t.debug) ?? !1;
33
+ }
34
+ /**
35
+ * Initialize audio context (create and ensure it's ready)
36
+ */
37
+ async initialize() {
38
+ this.audioContext || (this.audioContext = new AudioContext({
39
+ sampleRate: this.sampleRate
40
+ }), this.audioContext.state === "suspended" && await this.audioContext.resume(), this.log("AudioContext initialized", {
41
+ sessionId: this.sessionId,
42
+ sampleRate: this.audioContext.sampleRate,
43
+ state: this.audioContext.state
44
+ }));
45
+ }
46
+ /**
47
+ * Add audio chunk (16-bit PCM)
48
+ */
49
+ addChunk(t, e = !1) {
50
+ if (!this.audioContext) {
51
+ r.error("AudioContext not initialized");
52
+ return;
53
+ }
54
+ this.audioChunks.push({ data: t, isLast: e }), this.log(`Added chunk ${this.audioChunks.length}`, {
55
+ size: t.length,
56
+ totalChunks: this.audioChunks.length,
57
+ isLast: e,
58
+ isPlaying: this.isPlaying,
59
+ scheduledChunks: this.scheduledChunks
60
+ }), !this.isPlaying && this.audioChunks.length > 0 ? (this.log("[StreamingAudioPlayer] Auto-starting playback from addChunk"), this.startPlayback()) : this.isPlaying ? (this.log("[StreamingAudioPlayer] Already playing, scheduling next chunk"), this.scheduleNextChunk()) : this.log("[StreamingAudioPlayer] Not playing and no chunks, waiting for more chunks");
61
+ }
62
+ /**
63
+ * Start new session (stop current and start fresh)
64
+ */
65
+ async startNewSession(t) {
66
+ this.stop(), this.sessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, this.audioChunks = [], this.scheduledChunks = 0, this.pausedTimeOffset = 0, this.pausedAt = 0, this.log("Starting new session", {
67
+ chunks: t.length
68
+ });
69
+ for (const e of t)
70
+ this.addChunk(e.data, e.isLast);
71
+ }
72
+ /**
73
+ * Start playback
74
+ */
75
+ startPlayback() {
76
+ if (!this.audioContext) {
77
+ this.log("[StreamingAudioPlayer] Cannot start playback: AudioContext not initialized");
78
+ return;
79
+ }
80
+ if (this.isPlaying) {
81
+ this.log("[StreamingAudioPlayer] Cannot start playback: Already playing");
82
+ return;
83
+ }
84
+ this.isPlaying = !0, this.sessionStartTime = this.audioContext.currentTime, this.scheduledTime = this.sessionStartTime, this.log("[StreamingAudioPlayer] Starting playback", {
85
+ sessionStartTime: this.sessionStartTime,
86
+ bufferedChunks: this.audioChunks.length,
87
+ scheduledChunks: this.scheduledChunks,
88
+ activeSources: this.activeSources.length
89
+ }), this.scheduleAllChunks();
90
+ }
91
+ /**
92
+ * Schedule all pending chunks
93
+ */
94
+ scheduleAllChunks() {
95
+ for (; this.scheduledChunks < this.audioChunks.length; )
96
+ this.scheduleNextChunk();
97
+ }
98
+ /**
99
+ * Schedule next audio chunk
100
+ */
101
+ scheduleNextChunk() {
102
+ if (!this.audioContext) {
103
+ this.log("[StreamingAudioPlayer] Cannot schedule chunk: AudioContext not initialized");
104
+ return;
105
+ }
106
+ if (!this.isPlaying) {
107
+ this.log("[StreamingAudioPlayer] Cannot schedule chunk: Not playing");
108
+ return;
109
+ }
110
+ const t = this.scheduledChunks;
111
+ if (t >= this.audioChunks.length) {
112
+ this.log(`[StreamingAudioPlayer] No more chunks to schedule (chunkIndex: ${t}, totalChunks: ${this.audioChunks.length})`);
113
+ return;
114
+ }
115
+ const e = this.audioChunks[t];
116
+ if (e.data.length === 0 && !e.isLast) {
117
+ this.scheduledChunks++;
118
+ return;
119
+ }
120
+ const l = e.data, d = e.isLast, n = this.pcmToAudioBuffer(l);
121
+ if (!n) {
122
+ r.error("Failed to create AudioBuffer from PCM data");
123
+ return;
124
+ }
125
+ try {
126
+ const i = this.audioContext.createBufferSource();
127
+ i.buffer = n, i.connect(this.audioContext.destination), i.start(this.scheduledTime), this.activeSources.push(i), i.onended = () => {
128
+ const u = this.activeSources.indexOf(i);
129
+ u >= 0 && this.activeSources.splice(u, 1), d && this.activeSources.length === 0 && (this.log("Last audio chunk ended, marking playback as ended"), this.markEnded());
130
+ }, this.scheduledTime += n.duration, this.scheduledChunks++, this.log(`[StreamingAudioPlayer] Scheduled chunk ${t + 1}/${this.audioChunks.length}`, {
131
+ startTime: this.scheduledTime - n.duration,
132
+ duration: n.duration,
133
+ nextScheduleTime: this.scheduledTime,
134
+ isLast: d,
135
+ activeSources: this.activeSources.length
136
+ });
137
+ } catch (i) {
138
+ r.errorWithError("Failed to schedule audio chunk:", i);
139
+ }
140
+ }
141
+ /**
142
+ * Convert PCM data to AudioBuffer
143
+ * Input: 16-bit PCM (int16), Output: AudioBuffer (float32 [-1, 1])
144
+ */
145
+ pcmToAudioBuffer(t) {
146
+ if (!this.audioContext)
147
+ return null;
148
+ if (t.length === 0) {
149
+ const u = Math.floor(this.sampleRate * 0.01), a = this.audioContext.createBuffer(
150
+ this.channelCount,
151
+ u,
152
+ this.sampleRate
153
+ );
154
+ for (let o = 0; o < this.channelCount; o++)
155
+ a.getChannelData(o).fill(0);
156
+ return a;
157
+ }
158
+ const e = new Uint8Array(t), l = new Int16Array(e.buffer, 0, e.length / 2), d = l.length / this.channelCount, n = this.audioContext.createBuffer(
159
+ this.channelCount,
160
+ d,
161
+ this.sampleRate
162
+ );
163
+ for (let i = 0; i < this.channelCount; i++) {
164
+ const u = n.getChannelData(i);
165
+ for (let a = 0; a < d; a++) {
166
+ const o = a * this.channelCount + i;
167
+ u[a] = l[o] / 32768;
168
+ }
169
+ }
170
+ return n;
171
+ }
172
+ /**
173
+ * Get current playback time (seconds)
174
+ */
175
+ getCurrentTime() {
176
+ if (!this.audioContext || !this.isPlaying)
177
+ return 0;
178
+ if (this.isPaused)
179
+ return this.pausedAt;
180
+ const e = this.audioContext.currentTime - this.sessionStartTime - this.pausedTimeOffset;
181
+ return Math.max(0, e);
182
+ }
183
+ /**
184
+ * Pause playback
185
+ */
186
+ pause() {
187
+ !this.isPlaying || this.isPaused || (this.isPaused = !0, this.pausedAt = this.getCurrentTime(), this.log("Playback paused", {
188
+ pausedAt: this.pausedAt
189
+ }));
190
+ }
191
+ /**
192
+ * Resume playback
193
+ */
194
+ async resume() {
195
+ if (!this.isPaused || !this.audioContext)
196
+ return;
197
+ const t = this.audioContext.currentTime - (this.sessionStartTime + this.pausedAt);
198
+ this.pausedTimeOffset += t, this.isPaused = !1, this.log("Playback resumed", {
199
+ pauseDuration: t,
200
+ totalPausedOffset: this.pausedTimeOffset
201
+ });
202
+ }
203
+ /**
204
+ * Stop playback
205
+ */
206
+ stop() {
207
+ if (this.audioContext) {
208
+ this.log("[StreamingAudioPlayer] Stopping playback", {
209
+ isPlaying: this.isPlaying,
210
+ audioChunks: this.audioChunks.length,
211
+ scheduledChunks: this.scheduledChunks,
212
+ activeSources: this.activeSources.length
213
+ }), this.isPlaying = !1, this.isPaused = !1, this.sessionStartTime = 0, this.scheduledTime = 0;
214
+ for (const t of this.activeSources) {
215
+ t.onended = null;
216
+ try {
217
+ t.stop(0);
218
+ } catch {
219
+ }
220
+ try {
221
+ t.disconnect();
222
+ } catch {
223
+ }
224
+ }
225
+ this.activeSources = [], this.audioChunks = [], this.scheduledChunks = 0, this.log("[StreamingAudioPlayer] Playback stopped, state reset");
226
+ }
227
+ }
228
+ /**
229
+ * Mark playback as ended
230
+ */
231
+ markEnded() {
232
+ var t;
233
+ this.log("Playback ended"), this.isPlaying = !1, (t = this.onEndedCallback) == null || t.call(this);
234
+ }
235
+ /**
236
+ * Set ended callback
237
+ */
238
+ onEnded(t) {
239
+ this.onEndedCallback = t;
240
+ }
241
+ /**
242
+ * Check if playing
243
+ */
244
+ isPlayingNow() {
245
+ return this.isPlaying && !this.isPaused;
246
+ }
247
+ /**
248
+ * Get total duration of buffered audio
249
+ */
250
+ getBufferedDuration() {
251
+ if (!this.audioContext)
252
+ return 0;
253
+ let t = 0;
254
+ for (const e of this.audioChunks)
255
+ t += e.data.length / 2 / this.channelCount;
256
+ return t / this.sampleRate;
257
+ }
258
+ /**
259
+ * Get remaining duration (buffered - played) in seconds
260
+ */
261
+ getRemainingDuration() {
262
+ const t = this.getBufferedDuration(), e = this.getCurrentTime();
263
+ return Math.max(0, t - e);
264
+ }
265
+ /**
266
+ * Dispose and cleanup
267
+ */
268
+ dispose() {
269
+ this.stop(), this.audioContext && (this.audioContext.close(), this.audioContext = null), this.audioChunks = [], this.scheduledChunks = 0, this.sessionStartTime = 0, this.pausedTimeOffset = 0, this.pausedAt = 0, this.scheduledTime = 0, this.onEndedCallback = void 0, this.log("StreamingAudioPlayer disposed");
270
+ }
271
+ /**
272
+ * Flush buffered audio
273
+ * - hard: stops all playing sources and clears all chunks
274
+ * - soft (default): clears UNSCHEDULED chunks only
275
+ */
276
+ flush(t) {
277
+ if ((t == null ? void 0 : t.hard) === !0) {
278
+ this.stop(), this.audioChunks = [], this.scheduledChunks = 0, this.sessionStartTime = 0, this.pausedAt = 0, this.scheduledTime = 0, this.log("Flushed (hard)");
279
+ return;
280
+ }
281
+ this.scheduledChunks < this.audioChunks.length && this.audioChunks.splice(this.scheduledChunks), this.log("Flushed (soft)", { remainingScheduled: this.scheduledChunks });
282
+ }
283
+ /**
284
+ * Debug logging
285
+ */
286
+ log(t, e) {
287
+ this.debug && r.log(`[StreamingAudioPlayer] ${t}`, e || "");
288
+ }
289
+ }
290
+ export {
291
+ k as StreamingAudioPlayer
292
+ };
293
+ //# sourceMappingURL=StreamingAudioPlayer-C2TfYsO8.js.map