interview-widget 3.3.2 → 3.3.3-beta.0
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 +15 -7
- package/dist/StreamingAudioPlayer-DmOtqI_a-BX7ptCkK.js +430 -0
- package/dist/_headers +2 -0
- package/dist/assets/avatar_core_wasm-36bf5449.wasm +0 -0
- package/dist/assets/avatar_core_wasm.js +2697 -0
- package/dist/assets/index.d.ts +0 -11
- package/dist/assets/style-8JJ3h3Ol.css +1 -0
- package/dist/avatar_core_wasm-BIbE25D3-D94zNSYX.js +1651 -0
- package/dist/components/interview/interview-content.d.ts +8 -0
- package/dist/components/interview/interview-controller.d.ts +8 -1
- package/dist/components/interview/interview-header.d.ts +1 -1
- package/dist/components/interview/proctoring/gaze-detection/utils/types.d.ts +1 -10
- package/dist/components/interview/proctoring/interview-proctoring.d.ts +1 -0
- package/dist/components/interview/question-text.d.ts +4 -0
- package/dist/components/interview/transcript/interview-transcript.d.ts +1 -0
- package/dist/components/media/spatius-avatar-feed.d.ts +11 -0
- package/dist/components/media/video-feed.d.ts +4 -0
- package/dist/components/modals/onboarding-modal.d.ts +4 -1
- package/dist/components/ui/extended/chat-ui.d.ts +1 -4
- package/dist/context/interview-widget-context.d.ts +0 -8
- package/dist/context/proctoring-context.d.ts +1 -22
- package/dist/hooks/index.d.ts +2 -1
- package/dist/hooks/use-network-quality.d.ts +2 -1
- package/dist/hooks/use-next-question-stream.d.ts +11 -0
- package/dist/hooks/use-question-stream.d.ts +20 -0
- package/dist/hooks/use-spatius-question-stream.d.ts +22 -0
- package/dist/hooks/use-timer.d.ts +3 -2
- package/dist/hooks/use-tts.d.ts +1 -1
- package/dist/index-DXUDYyS8.js +17107 -0
- package/dist/services/api/endpoints.d.ts +2 -4
- package/dist/services/timer/index.d.ts +0 -1
- package/dist/services/tts/tts-service.d.ts +3 -2
- package/dist/types.d.ts +4 -0
- package/dist/utils/constants.d.ts +0 -5
- package/dist/utils/crypto.d.ts +0 -7
- package/dist/utils/helper.d.ts +0 -4
- package/dist/widget.es.3.3.3-beta.0.js +10532 -0
- package/dist/widget.umd.3.3.3-beta.0.js +783 -0
- package/package.json +10 -11
- package/dist/assets/clock-icon.d.ts +0 -1
- package/dist/assets/focus-icon.d.ts +0 -1
- package/dist/assets/info-icon.d.ts +0 -1
- package/dist/assets/keyboard-icon.d.ts +0 -1
- package/dist/assets/mic-off-icon.d.ts +0 -1
- package/dist/assets/monitor-icon.d.ts +0 -1
- package/dist/assets/mouse-pointer-click-icon.d.ts +0 -1
- package/dist/assets/scan-face-icon.d.ts +0 -1
- package/dist/assets/shield-icon.d.ts +0 -2
- package/dist/assets/speak-icon.d.ts +0 -1
- package/dist/assets/video-off-icon.d.ts +0 -1
- package/dist/assets/volumn-icon.d.ts +0 -1
- package/dist/components/interview/answer-area.d.ts +0 -12
- package/dist/components/interview/phases/interview-completion-modal.d.ts +0 -7
- package/dist/components/interview/proctoring/face-data-permission-modal.d.ts +0 -7
- package/dist/components/ui/dialog.d.ts +0 -13
- package/dist/components/ui/input.d.ts +0 -8
- package/dist/components/ui/media-control-item.d.ts +0 -8
- package/dist/hooks/use-dialog.d.ts +0 -6
- package/dist/styles.d.ts +0 -0
- package/dist/utils/audio-storage.d.ts +0 -24
- package/dist/widget.3.3.2.css +0 -1
- package/dist/widget.es.3.3.2.js +0 -9795
- package/dist/widget.umd.3.3.2.js +0 -179
package/README.md
CHANGED
|
@@ -72,6 +72,7 @@ export default function App() {
|
|
|
72
72
|
language: "en",
|
|
73
73
|
},
|
|
74
74
|
tts: { provider: "piper" },
|
|
75
|
+
avatar: { provider: "none" },
|
|
75
76
|
timers: {
|
|
76
77
|
thinkingDuration: 30,
|
|
77
78
|
answeringDuration: 120,
|
|
@@ -201,13 +202,13 @@ Pass this object to `InterviewWidgetProvider` via the `config` prop. Tables list
|
|
|
201
202
|
|
|
202
203
|
### UI
|
|
203
204
|
|
|
204
|
-
| Prop
|
|
205
|
-
|
|
|
206
|
-
| baseColor
|
|
207
|
-
| borderRadius
|
|
208
|
-
| retryProgressSavedText
|
|
209
|
-
| retryMaxAttempts
|
|
210
|
-
| retryNoAttemptsLeftText | `string`
|
|
205
|
+
| Prop | Type | Default | Description |
|
|
206
|
+
| ----------------------- | -------------- | --------------------------------------------------------------- | ------------------------------------------------------------- |
|
|
207
|
+
| baseColor | `string (hex)` | "#3B82F6" | Primary brand color used by the widget |
|
|
208
|
+
| borderRadius | `string (CSS)` | "8px" | Global corner radius for components |
|
|
209
|
+
| retryProgressSavedText | `string` | `"Your progress is saved — tap Retry or exit the interview..."` | Text to show in next-question retry modal |
|
|
210
|
+
| retryMaxAttempts | `number` | 3 | Maximum number of attempts allowed for retrying next question |
|
|
211
|
+
| retryNoAttemptsLeftText | `string` | `"Retry limit reached. Please exit and resume later."` | Error message shown when max attempts are reached |
|
|
211
212
|
|
|
212
213
|
### Interview
|
|
213
214
|
|
|
@@ -219,6 +220,7 @@ Pass this object to `InterviewWidgetProvider` via the `config` prop. Tables list
|
|
|
219
220
|
| timers | `object` | [see below](#timers) | Per-phase and global timing configuration |
|
|
220
221
|
| stt | `object` | [see below](#stt-speech-to-text) | Speech-to-Text provider settings |
|
|
221
222
|
| tts | `object` | [see below](#tts-text-to-speech) | Text-to-Speech provider settings |
|
|
223
|
+
| avatar | `object` | [see below](#avatar) | Optional avatar question-delivery provider |
|
|
222
224
|
| proctoring | `object` | [see below](#proctoring) | Proctoring settings and cheating detection |
|
|
223
225
|
|
|
224
226
|
#### Timers
|
|
@@ -239,6 +241,12 @@ Pass this object to `InterviewWidgetProvider` via the `config` prop. Tables list
|
|
|
239
241
|
| model | `"whisper-large-v3-turbo" \| string` | "whisper-large-v3-turbo" | STT model identifier |
|
|
240
242
|
| language | `string` | "en" | Language code passed to the STT engine |
|
|
241
243
|
|
|
244
|
+
#### Avatar
|
|
245
|
+
|
|
246
|
+
| Prop | Type | Default | Description |
|
|
247
|
+
| -------- | ------------------------- | -------- | ------------------------------------------------------------------------------------------- |
|
|
248
|
+
| provider | `"none" \| "spatius"` | `"none"` | Keep the standard SSE/audio flow or render streamed questions through Spatius Direct Mode. |
|
|
249
|
+
|
|
242
250
|
#### TTS (Text-To-Speech)
|
|
243
251
|
|
|
244
252
|
| Prop | Type | Default | Description |
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
import { A as C, l as a, e as g, a as c } from "./index-DXUDYyS8.js";
|
|
2
|
+
var m = Object.defineProperty, f = (r, t, e) => t in r ? m(r, t, { enumerable: !0, configurable: !0, writable: !0, value: e }) : r[t] = e, s = (r, t, e) => f(r, typeof t != "symbol" ? t + "" : t, e);
|
|
3
|
+
class p {
|
|
4
|
+
// Mark if AudioContext is being resumed, avoid concurrent resume requests
|
|
5
|
+
constructor(t) {
|
|
6
|
+
s(this, "audioContext", null), s(this, "sampleRate"), s(this, "channelCount"), s(this, "debug"), s(this, "sessionId"), s(this, "sessionStartTime", 0), s(this, "pausedTimeOffset", 0), s(this, "pausedAt", 0), s(this, "pausedAudioContextTime", 0), s(this, "scheduledTime", 0), s(this, "isPlaying", !1), s(this, "isPaused", !1), s(this, "autoStartEnabled", !0), s(this, "autoContinue", !1), s(this, "audioChunks", []), s(this, "scheduledChunks", 0), s(this, "activeSources", /* @__PURE__ */ new Set()), s(this, "lastScheduledChunkEndTime", 0), s(this, "lastGetCurrentTimeLog", 0), s(this, "scheduledChunkInfo", []), s(this, "gainNode", null), s(this, "volume", 1), s(this, "onEndedCallback"), s(this, "stateChangeHandler"), s(this, "isResuming", !1), this.sessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, this.sampleRate = (t == null ? void 0 : t.sampleRate) ?? C.audio.sampleRate, this.channelCount = (t == null ? void 0 : t.channelCount) ?? 1, this.debug = (t == null ? void 0 : t.debug) ?? !1;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Initialize audio context (create and ensure it's ready)
|
|
10
|
+
*/
|
|
11
|
+
async initialize() {
|
|
12
|
+
if (!this.audioContext)
|
|
13
|
+
try {
|
|
14
|
+
this.audioContext = new AudioContext({
|
|
15
|
+
sampleRate: this.sampleRate
|
|
16
|
+
}), this.gainNode = this.audioContext.createGain(), this.gainNode.gain.value = this.volume, this.gainNode.connect(this.audioContext.destination), this.audioContext.state === "suspended" && await this.audioContext.resume(), this.stateChangeHandler = (t) => {
|
|
17
|
+
t.target.state === "suspended" && this.isPlaying && !this.isPaused && this.ensureAudioContextRunning().catch((i) => {
|
|
18
|
+
a.error("[StreamingAudioPlayer] Failed to auto-resume AudioContext after external suspend:", i);
|
|
19
|
+
});
|
|
20
|
+
}, this.audioContext.addEventListener("statechange", this.stateChangeHandler), this.log("AudioContext initialized", {
|
|
21
|
+
sessionId: this.sessionId,
|
|
22
|
+
sampleRate: this.audioContext.sampleRate,
|
|
23
|
+
state: this.audioContext.state
|
|
24
|
+
});
|
|
25
|
+
} catch (t) {
|
|
26
|
+
const e = g(t);
|
|
27
|
+
throw c("audio_session_init_failed", "warning", {
|
|
28
|
+
session_id: this.sessionId,
|
|
29
|
+
reason: e
|
|
30
|
+
}), a.error("Failed to initialize AudioContext:", e), t instanceof Error ? t : new Error(e);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Ensure AudioContext is running (auto-resume if suspended)
|
|
35
|
+
* Only auto-resume when playing and not paused, avoid interfering with normal pause/resume logic
|
|
36
|
+
*
|
|
37
|
+
* Optimizations:
|
|
38
|
+
* - Fast path: if already in running state, return directly
|
|
39
|
+
* - Avoid concurrent resume: use isResuming flag to prevent duplicate resume requests
|
|
40
|
+
* - Handle closed state: if AudioContext is closed, cannot resume
|
|
41
|
+
* @internal
|
|
42
|
+
*/
|
|
43
|
+
async ensureAudioContextRunning() {
|
|
44
|
+
if (!this.audioContext)
|
|
45
|
+
return;
|
|
46
|
+
const t = this.audioContext.state;
|
|
47
|
+
if (t !== "running") {
|
|
48
|
+
if (t === "closed") {
|
|
49
|
+
this.log("AudioContext is closed, cannot resume", {
|
|
50
|
+
sessionId: this.sessionId,
|
|
51
|
+
state: t
|
|
52
|
+
});
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (t === "suspended" && this.isPlaying && !this.isPaused) {
|
|
56
|
+
if (this.isResuming) {
|
|
57
|
+
this.log("AudioContext resume already in progress, skipping duplicate request", {
|
|
58
|
+
sessionId: this.sessionId,
|
|
59
|
+
state: t
|
|
60
|
+
});
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
this.isResuming = !0;
|
|
64
|
+
try {
|
|
65
|
+
this.log("AudioContext is suspended during playback, resuming...", {
|
|
66
|
+
sessionId: this.sessionId,
|
|
67
|
+
state: t,
|
|
68
|
+
isPlaying: this.isPlaying,
|
|
69
|
+
isPaused: this.isPaused
|
|
70
|
+
}), await this.audioContext.resume(), this.log("AudioContext resumed successfully", {
|
|
71
|
+
sessionId: this.sessionId,
|
|
72
|
+
state: this.audioContext.state
|
|
73
|
+
});
|
|
74
|
+
} catch (e) {
|
|
75
|
+
a.error("[StreamingAudioPlayer] Failed to resume AudioContext:", e), c("audio_context_resume_failed", "error", {
|
|
76
|
+
session_id: this.sessionId,
|
|
77
|
+
reason: e instanceof Error ? e.message : String(e)
|
|
78
|
+
});
|
|
79
|
+
} finally {
|
|
80
|
+
this.isResuming = !1;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Add audio chunk (16-bit PCM)
|
|
87
|
+
*/
|
|
88
|
+
addChunk(t, e = !1) {
|
|
89
|
+
if (!this.audioContext) {
|
|
90
|
+
a.error("AudioContext not initialized");
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
this.isPlaying && !this.isPaused && this.audioContext.state === "suspended" && this.ensureAudioContextRunning().catch((i) => {
|
|
94
|
+
a.error("[StreamingAudioPlayer] Failed to ensure AudioContext running in addChunk:", i);
|
|
95
|
+
}), this.audioChunks.push({ data: t, isLast: e }), this.log(`Added chunk ${this.audioChunks.length}`, {
|
|
96
|
+
size: t.length,
|
|
97
|
+
totalChunks: this.audioChunks.length,
|
|
98
|
+
isLast: e,
|
|
99
|
+
isPlaying: this.isPlaying,
|
|
100
|
+
scheduledChunks: this.scheduledChunks
|
|
101
|
+
}), this.autoContinue && this.isPaused && (this.log("[StreamingAudioPlayer] autoContinue=true, auto-resuming playback"), this.autoContinue = !1, this.resume().catch((i) => {
|
|
102
|
+
a.error("Failed to auto-resume playback:", i);
|
|
103
|
+
})), !this.isPlaying && this.autoStartEnabled && this.audioChunks.length > 0 ? (this.log("[StreamingAudioPlayer] Auto-starting playback from addChunk"), this.startPlayback().catch((i) => {
|
|
104
|
+
a.error("[StreamingAudioPlayer] Failed to start playback from addChunk:", i);
|
|
105
|
+
})) : this.isPlaying && !this.isPaused ? (this.log("[StreamingAudioPlayer] Already playing, scheduling next chunk"), this.scheduleNextChunk()) : this.log("[StreamingAudioPlayer] Not playing and no chunks, waiting for more chunks");
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Start new session (stop current and start fresh)
|
|
109
|
+
*/
|
|
110
|
+
async startNewSession(t, e = 0) {
|
|
111
|
+
if (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.pausedAudioContextTime = 0, this.autoContinue = !1, this.log("Starting new session", {
|
|
112
|
+
chunks: t.length,
|
|
113
|
+
leadingSilenceMs: e
|
|
114
|
+
}), e > 0) {
|
|
115
|
+
const i = Math.floor(e / 1e3 * this.sampleRate) * this.channelCount, u = new Uint8Array(i * 2);
|
|
116
|
+
this.addChunk(u, !1);
|
|
117
|
+
}
|
|
118
|
+
for (const i of t)
|
|
119
|
+
this.addChunk(i.data, i.isLast);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Start playback
|
|
123
|
+
*/
|
|
124
|
+
async startPlayback() {
|
|
125
|
+
if (!this.audioContext) {
|
|
126
|
+
this.log("[StreamingAudioPlayer] Cannot start playback: AudioContext not initialized");
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (this.isPlaying) {
|
|
130
|
+
this.log("[StreamingAudioPlayer] Cannot start playback: Already playing");
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
await this.ensureAudioContextRunning(), this.isPlaying = !0, this.sessionStartTime = this.audioContext.currentTime, this.scheduledTime = this.sessionStartTime, this.lastScheduledChunkEndTime = 0, this.scheduledChunkInfo = [], this.autoContinue = !1, this.log("[StreamingAudioPlayer] Starting playback", {
|
|
134
|
+
sessionStartTime: this.sessionStartTime,
|
|
135
|
+
bufferedChunks: this.audioChunks.length,
|
|
136
|
+
scheduledChunks: this.scheduledChunks,
|
|
137
|
+
activeSources: this.activeSources.size,
|
|
138
|
+
audioContextState: this.audioContext.state
|
|
139
|
+
}), this.scheduleAllChunks();
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Schedule all pending chunks
|
|
143
|
+
*/
|
|
144
|
+
scheduleAllChunks() {
|
|
145
|
+
for (; this.scheduledChunks < this.audioChunks.length; )
|
|
146
|
+
this.scheduleNextChunk();
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Schedule next audio chunk
|
|
150
|
+
*/
|
|
151
|
+
scheduleNextChunk() {
|
|
152
|
+
if (!this.audioContext) {
|
|
153
|
+
this.log("[StreamingAudioPlayer] Cannot schedule chunk: AudioContext not initialized");
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (!this.isPlaying || this.isPaused) {
|
|
157
|
+
this.log("[StreamingAudioPlayer] Cannot schedule chunk: Not playing or paused");
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
this.audioContext.state === "suspended" && this.ensureAudioContextRunning().catch((n) => {
|
|
161
|
+
a.error("[StreamingAudioPlayer] Failed to ensure AudioContext running in scheduleNextChunk:", n);
|
|
162
|
+
});
|
|
163
|
+
const t = this.scheduledChunks;
|
|
164
|
+
if (t >= this.audioChunks.length) {
|
|
165
|
+
this.log(`[StreamingAudioPlayer] No more chunks to schedule (chunkIndex: ${t}, totalChunks: ${this.audioChunks.length})`);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const e = this.audioChunks[t];
|
|
169
|
+
if (e.data.length === 0 && !e.isLast) {
|
|
170
|
+
this.scheduledChunks++;
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const i = e.data, u = e.isLast, o = this.pcmToAudioBuffer(i);
|
|
174
|
+
if (!o) {
|
|
175
|
+
a.error("Failed to create AudioBuffer from PCM data"), c("audio_buffer_creation_failed", "error", {
|
|
176
|
+
session_id: this.sessionId
|
|
177
|
+
});
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
try {
|
|
181
|
+
const n = this.audioContext.createBufferSource();
|
|
182
|
+
n.buffer = o, n.connect(this.gainNode);
|
|
183
|
+
const l = this.scheduledTime;
|
|
184
|
+
n.start(l);
|
|
185
|
+
const h = Math.max(l, this.audioContext.currentTime);
|
|
186
|
+
this.scheduledChunkInfo.push({
|
|
187
|
+
startTime: h,
|
|
188
|
+
duration: o.duration
|
|
189
|
+
}), this.activeSources.add(n), n.onended = () => {
|
|
190
|
+
if (this.activeSources.delete(n), this.activeSources.size === 0) {
|
|
191
|
+
const d = this.audioChunks[this.scheduledChunks - 1];
|
|
192
|
+
d && !d.isLast ? (this.log("All audio chunks ended but end=false, pausing and setting autoContinue"), c("audio_buffer_stall", "warning", {
|
|
193
|
+
scheduled_chunks: this.scheduledChunks,
|
|
194
|
+
audio_time: this.getCurrentTime()
|
|
195
|
+
}), this.autoContinue = !0, this.pause()) : u && (this.log("Last audio chunk ended, marking playback as ended"), this.markEnded());
|
|
196
|
+
}
|
|
197
|
+
}, this.scheduledTime += o.duration, this.lastScheduledChunkEndTime = this.scheduledTime - this.sessionStartTime - this.pausedTimeOffset, this.scheduledChunks++, this.log(`[StreamingAudioPlayer] Scheduled chunk ${t + 1}/${this.audioChunks.length}`, {
|
|
198
|
+
startTime: this.scheduledTime - o.duration,
|
|
199
|
+
duration: o.duration,
|
|
200
|
+
nextScheduleTime: this.scheduledTime,
|
|
201
|
+
isLast: u,
|
|
202
|
+
activeSources: this.activeSources.size
|
|
203
|
+
});
|
|
204
|
+
} catch (n) {
|
|
205
|
+
a.error("Failed to schedule audio chunk:", n), c("schedule_chunk_failed", "error", {
|
|
206
|
+
session_id: this.sessionId,
|
|
207
|
+
reason: n instanceof Error ? n.message : String(n)
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Convert PCM data to AudioBuffer
|
|
213
|
+
* Input: 16-bit PCM (int16), Output: AudioBuffer (float32 [-1, 1])
|
|
214
|
+
*/
|
|
215
|
+
pcmToAudioBuffer(t) {
|
|
216
|
+
if (!this.audioContext)
|
|
217
|
+
return null;
|
|
218
|
+
if (t.length === 0) {
|
|
219
|
+
const l = Math.floor(this.sampleRate * 0.01), h = this.audioContext.createBuffer(
|
|
220
|
+
this.channelCount,
|
|
221
|
+
l,
|
|
222
|
+
this.sampleRate
|
|
223
|
+
);
|
|
224
|
+
for (let d = 0; d < this.channelCount; d++)
|
|
225
|
+
h.getChannelData(d).fill(0);
|
|
226
|
+
return h;
|
|
227
|
+
}
|
|
228
|
+
const e = new Uint8Array(t), i = new Int16Array(e.buffer, 0, e.length / 2), u = i.length / this.channelCount, o = this.audioContext.createBuffer(
|
|
229
|
+
this.channelCount,
|
|
230
|
+
u,
|
|
231
|
+
this.sampleRate
|
|
232
|
+
);
|
|
233
|
+
for (let n = 0; n < this.channelCount; n++) {
|
|
234
|
+
const l = o.getChannelData(n);
|
|
235
|
+
for (let h = 0; h < u; h++) {
|
|
236
|
+
const d = h * this.channelCount + n;
|
|
237
|
+
l[h] = i[d] / 32768;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return o;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Get current playback time (seconds)
|
|
244
|
+
* Returns total actual playback duration
|
|
245
|
+
* @internal
|
|
246
|
+
*/
|
|
247
|
+
getCurrentTime() {
|
|
248
|
+
if (!this.audioContext || !this.isPlaying)
|
|
249
|
+
return 0;
|
|
250
|
+
if (this.isPaused)
|
|
251
|
+
return this.pausedAt;
|
|
252
|
+
const t = this.audioContext.currentTime;
|
|
253
|
+
if (this.activeSources.size === 0 && this.scheduledChunks > 0)
|
|
254
|
+
return Math.max(0, this.lastScheduledChunkEndTime);
|
|
255
|
+
let e = 0;
|
|
256
|
+
for (let i = 0; i < this.scheduledChunkInfo.length; i++) {
|
|
257
|
+
const u = this.scheduledChunkInfo[i], o = u.startTime + u.duration;
|
|
258
|
+
if (t < u.startTime)
|
|
259
|
+
break;
|
|
260
|
+
if (o <= t)
|
|
261
|
+
e += u.duration;
|
|
262
|
+
else {
|
|
263
|
+
const n = t - u.startTime;
|
|
264
|
+
e += n;
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return Math.max(0, e);
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Get total duration of buffered audio (seconds)
|
|
272
|
+
* Calculate total duration of all buffered chunks
|
|
273
|
+
* @internal
|
|
274
|
+
*/
|
|
275
|
+
getBufferedDuration() {
|
|
276
|
+
if (!this.audioContext)
|
|
277
|
+
return 0;
|
|
278
|
+
let t = 0;
|
|
279
|
+
for (const e of this.audioChunks) {
|
|
280
|
+
const i = e.data.length / (this.sampleRate * this.channelCount * 2);
|
|
281
|
+
t += i;
|
|
282
|
+
}
|
|
283
|
+
return t;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Get current AudioContext time
|
|
287
|
+
* @returns Current AudioContext time in seconds, or 0 if AudioContext is not initialized
|
|
288
|
+
*/
|
|
289
|
+
getAudioContextTime() {
|
|
290
|
+
var t;
|
|
291
|
+
return ((t = this.audioContext) == null ? void 0 : t.currentTime) ?? 0;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Pause playback
|
|
295
|
+
*/
|
|
296
|
+
pause() {
|
|
297
|
+
!this.isPlaying || this.isPaused || !this.audioContext || (this.pausedAt = this.getCurrentTime(), this.pausedAudioContextTime = this.audioContext.currentTime, this.isPaused = !0, this.audioContext.state === "running" && this.audioContext.suspend().catch((t) => {
|
|
298
|
+
a.error("Failed to suspend AudioContext:", t), this.isPaused = !1;
|
|
299
|
+
}), this.log("Playback paused", {
|
|
300
|
+
pausedAt: this.pausedAt,
|
|
301
|
+
pausedAudioContextTime: this.pausedAudioContextTime,
|
|
302
|
+
audioContextState: this.audioContext.state
|
|
303
|
+
}));
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Resume playback
|
|
307
|
+
*/
|
|
308
|
+
async resume() {
|
|
309
|
+
if (!this.isPaused || !this.audioContext || !this.isPlaying)
|
|
310
|
+
return;
|
|
311
|
+
if (this.autoContinue = !1, this.audioContext.state === "suspended")
|
|
312
|
+
try {
|
|
313
|
+
await this.audioContext.resume();
|
|
314
|
+
} catch (e) {
|
|
315
|
+
throw a.error("Failed to resume AudioContext:", e), e;
|
|
316
|
+
}
|
|
317
|
+
const t = this.audioContext.currentTime;
|
|
318
|
+
this.sessionStartTime = this.pausedAudioContextTime - this.pausedAt - this.pausedTimeOffset, this.isPaused = !1, this.scheduledChunks < this.audioChunks.length && this.scheduleAllChunks(), this.log("Playback resumed", {
|
|
319
|
+
pausedAt: this.pausedAt,
|
|
320
|
+
pausedAudioContextTime: this.pausedAudioContextTime,
|
|
321
|
+
currentAudioContextTime: t,
|
|
322
|
+
adjustedSessionStartTime: this.sessionStartTime,
|
|
323
|
+
audioContextState: this.audioContext.state
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Stop playback
|
|
328
|
+
*/
|
|
329
|
+
stop() {
|
|
330
|
+
if (this.audioContext) {
|
|
331
|
+
this.isPaused && this.audioContext.state === "suspended" && (this.audioContext.resume().catch(() => {
|
|
332
|
+
}), this.isPaused = !1), this.isPlaying = !1, this.isPaused = !1, this.isResuming = !1, this.sessionStartTime = 0, this.scheduledTime = 0;
|
|
333
|
+
for (const t of this.activeSources) {
|
|
334
|
+
t.onended = null;
|
|
335
|
+
try {
|
|
336
|
+
t.stop(0);
|
|
337
|
+
} catch {
|
|
338
|
+
}
|
|
339
|
+
try {
|
|
340
|
+
t.disconnect();
|
|
341
|
+
} catch {
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
this.activeSources.clear(), this.audioChunks = [], this.scheduledChunks = 0, this.autoContinue = !1, this.log("[StreamingAudioPlayer] Playback stopped, state reset");
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Enable or disable auto-start (for delayed start scenarios)
|
|
349
|
+
*/
|
|
350
|
+
setAutoStart(t) {
|
|
351
|
+
this.autoStartEnabled = t, this.log(`Auto-start ${t ? "enabled" : "disabled"}`);
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Start playback manually (for delayed start scenarios)
|
|
355
|
+
* This allows starting playback after transition animation completes
|
|
356
|
+
*/
|
|
357
|
+
async play() {
|
|
358
|
+
if (!this.isPlaying) {
|
|
359
|
+
this.autoStartEnabled = !0;
|
|
360
|
+
try {
|
|
361
|
+
await this.startPlayback();
|
|
362
|
+
} catch (t) {
|
|
363
|
+
a.error("[StreamingAudioPlayer] Failed to start playback from play():", t);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Mark playback as ended
|
|
369
|
+
*/
|
|
370
|
+
markEnded() {
|
|
371
|
+
var t;
|
|
372
|
+
this.log("Playback ended"), this.isPlaying = !1, (t = this.onEndedCallback) == null || t.call(this);
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Set ended callback
|
|
376
|
+
*/
|
|
377
|
+
onEnded(t) {
|
|
378
|
+
this.onEndedCallback = t;
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Check if playing
|
|
382
|
+
*/
|
|
383
|
+
isPlayingNow() {
|
|
384
|
+
return this.isPlaying && !this.isPaused;
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Dispose and cleanup
|
|
388
|
+
*/
|
|
389
|
+
dispose() {
|
|
390
|
+
this.stop(), this.audioContext && this.stateChangeHandler && (this.audioContext.removeEventListener("statechange", this.stateChangeHandler), this.stateChangeHandler = void 0), this.audioContext && (this.audioContext.close(), this.audioContext = null, this.gainNode = null), this.audioChunks = [], this.scheduledChunks = 0, this.sessionStartTime = 0, this.pausedTimeOffset = 0, this.pausedAt = 0, this.pausedAudioContextTime = 0, this.scheduledTime = 0, this.onEndedCallback = void 0, this.log("StreamingAudioPlayer disposed");
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Flush buffered audio
|
|
394
|
+
* - hard: stops all playing sources and clears all chunks
|
|
395
|
+
* - soft (default): clears UNSCHEDULED chunks only
|
|
396
|
+
*/
|
|
397
|
+
flush(t) {
|
|
398
|
+
if ((t == null ? void 0 : t.hard) === !0) {
|
|
399
|
+
this.stop(), this.audioChunks = [], this.scheduledChunks = 0, this.sessionStartTime = 0, this.pausedAt = 0, this.scheduledTime = 0, this.log("Flushed (hard)");
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
this.scheduledChunks < this.audioChunks.length && this.audioChunks.splice(this.scheduledChunks), this.log("Flushed (soft)", { remainingScheduled: this.scheduledChunks });
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Set volume (0.0 - 1.0)
|
|
406
|
+
* Note: This only controls avatar audio player volume, does not affect system volume
|
|
407
|
+
* @param volume Volume value, range 0.0 to 1.0 (0.0 is mute, 1.0 is max volume)
|
|
408
|
+
* @internal
|
|
409
|
+
*/
|
|
410
|
+
setVolume(t) {
|
|
411
|
+
(t < 0 || t > 1) && (a.warn(`[StreamingAudioPlayer] Volume out of range: ${t}, clamping to [0, 1]`), t = Math.max(0, Math.min(1, t))), this.volume = t, this.gainNode && (this.gainNode.gain.value = t);
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Get current volume
|
|
415
|
+
* @returns Current volume value (0.0 - 1.0)
|
|
416
|
+
* @internal
|
|
417
|
+
*/
|
|
418
|
+
getVolume() {
|
|
419
|
+
return this.volume;
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Debug logging
|
|
423
|
+
*/
|
|
424
|
+
log(t, e) {
|
|
425
|
+
this.debug && a.log(`[StreamingAudioPlayer] ${t}`, e || "");
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
export {
|
|
429
|
+
p as StreamingAudioPlayer
|
|
430
|
+
};
|
package/dist/_headers
ADDED
|
Binary file
|