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++) {