diff --git a/examples/next-app/components/ExampleComponent.tsx b/examples/next-app/components/ExampleComponent.tsx
index daf82185..44944465 100644
--- a/examples/next-app/components/ExampleComponent.tsx
+++ b/examples/next-app/components/ExampleComponent.tsx
@@ -41,6 +41,7 @@ export const ExampleComponent = () => {
isPaused,
volume,
setVolume,
+ playerQueueLength,
} = useVoice();
const [textValue, setTextValue] = useState('');
@@ -87,6 +88,12 @@ export const ExampleComponent = () => {
{isPlaying ? 'true' : 'false'}
+
+
+ Player queue length
+
+
{playerQueueLength}
+
Ready state
diff --git a/packages/react/src/lib/VoiceProvider.tsx b/packages/react/src/lib/VoiceProvider.tsx
index fbaab539..50295635 100644
--- a/packages/react/src/lib/VoiceProvider.tsx
+++ b/packages/react/src/lib/VoiceProvider.tsx
@@ -86,6 +86,7 @@ export type VoiceContextType = {
callDurationTimestamp: string | null;
toolStatusStore: ReturnType
['store'];
chatMetadata: ChatMetadataMessage | null;
+ playerQueueLength: number;
isPaused: boolean;
volume: number;
setVolume: (level: number) => void;
@@ -540,6 +541,7 @@ export const VoiceProvider: FC = ({
callDurationTimestamp,
toolStatusStore: toolStatus.store,
chatMetadata: messageStore.chatMetadata,
+ playerQueueLength: player.queueLength,
isPaused,
volume: player.volume,
setVolume: player.setVolume,
@@ -552,6 +554,9 @@ export const VoiceProvider: FC = ({
player.isPlaying,
player.muteAudio,
player.unmuteAudio,
+ player.queueLength,
+ player.volume,
+ player.setVolume,
mic.fft,
mic.isMuted,
mic.mute,
@@ -577,8 +582,6 @@ export const VoiceProvider: FC = ({
callDurationTimestamp,
toolStatus.store,
isPaused,
- player.volume,
- player.setVolume,
],
);
diff --git a/packages/react/src/lib/useSoundPlayer.test.ts b/packages/react/src/lib/useSoundPlayer.test.ts
index 0f1401b3..4818e131 100644
--- a/packages/react/src/lib/useSoundPlayer.test.ts
+++ b/packages/react/src/lib/useSoundPlayer.test.ts
@@ -21,6 +21,7 @@ describe('useSoundPlayer', () => {
expect(result.current.volume).toBe(1.0); // full volume
expect(result.current.isAudioMuted).toBe(false); // not muted
expect(result.current.isPlaying).toBe(false); // not playing
+ expect(result.current.queueLength).toBe(0); // empty queue
expect(result.current.fft).toEqual(generateEmptyFft()); // empty fft
});
});
diff --git a/packages/react/src/lib/useSoundPlayer.ts b/packages/react/src/lib/useSoundPlayer.ts
index 069e08c5..4dcb4834 100644
--- a/packages/react/src/lib/useSoundPlayer.ts
+++ b/packages/react/src/lib/useSoundPlayer.ts
@@ -1,5 +1,6 @@
import { convertBase64ToBlob } from 'hume';
import { useCallback, useRef, useState } from 'react';
+import z from 'zod';
import { convertLinearFrequenciesToBark } from './convertFrequencyScale';
import { generateEmptyFft } from './generateEmptyFft';
@@ -14,6 +15,7 @@ export const useSoundPlayer = (props: {
const [isAudioMuted, setIsAudioMuted] = useState(false);
const [volume, setVolumeState] = useState(1.0);
const [fft, setFft] = useState(generateEmptyFft());
+ const [queueLength, setQueueLength] = useState(0);
const audioContext = useRef(null);
const analyserNode = useRef(null);
@@ -52,7 +54,7 @@ export const useSoundPlayer = (props: {
await initAudioContext.audioWorklet
.addModule(
- 'https://storage.googleapis.com/evi-react-sdk-assets/audio-worklet-20250506.js',
+ 'https://storage.googleapis.com/evi-react-sdk-assets/audio-worklet-20250507.js',
)
.catch((e) => {
console.log(e);
@@ -63,11 +65,29 @@ export const useSoundPlayer = (props: {
workletNode.current = worklet;
worklet.port.onmessage = (e: MessageEvent) => {
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
- if (e.data?.type === 'ended') {
+ const endedEvent = z
+ .object({
+ type: z.literal('ended'),
+ })
+ .safeParse(e.data);
+
+ if (endedEvent.success) {
setIsPlaying(false);
onStopAudio.current('stream');
}
+
+ const queueLengthEvent = z
+ .object({
+ type: z.literal('queueLength'),
+ length: z.number(),
+ })
+ .safeParse(e.data);
+ if (queueLengthEvent.success) {
+ if (queueLengthEvent.data.length === 0) {
+ setIsPlaying(false);
+ }
+ setQueueLength(queueLengthEvent.data.length);
+ }
};
frequencyDataIntervalId.current = window.setInterval(() => {
@@ -205,5 +225,6 @@ export const useSoundPlayer = (props: {
clearQueue,
volume,
setVolume,
+ queueLength,
};
};
diff --git a/packages/react/src/worklets/audio-worklet.js b/packages/react/src/worklets/audio-worklet.js
index 2b4f3d1d..c6a8f112 100644
--- a/packages/react/src/worklets/audio-worklet.js
+++ b/packages/react/src/worklets/audio-worklet.js
@@ -17,6 +17,10 @@ class BufferQueue {
this._hasPushed = false;
}
+ get size() {
+ return this._buffers.length;
+ }
+
read() {
if (!this._hasPushed) {
return null;
@@ -85,6 +89,7 @@ class AudioStreamProcessor extends AudioWorkletProcessor {
this._fadeOutActive = false;
this._fadeOutCounter = 0;
}
+ this.port.postMessage({ type: 'queueLength', length: this._bq.size });
break;
case 'end':
this._shouldStop = true;
@@ -118,6 +123,7 @@ class AudioStreamProcessor extends AudioWorkletProcessor {
const channels = output.length;
const block = this._bq.read();
+ this.port.postMessage({ type: 'queueLength', length: this._bq.size });
if (block) {
for (let ch = 0; ch < channels; ch++) {