feat: add voice detection gating for TTS playback and optimize agent screen rendering
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useMemo } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { View, Text } from "react-native";
|
import { View, Text, ActivityIndicator } from "react-native";
|
||||||
import { useLocalSearchParams } from "expo-router";
|
import { useLocalSearchParams } from "expo-router";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import { useReanimatedKeyboardAnimation } from "react-native-keyboard-controller";
|
import { useReanimatedKeyboardAnimation } from "react-native-keyboard-controller";
|
||||||
@@ -18,6 +18,7 @@ export default function AgentScreen() {
|
|||||||
const { id } = useLocalSearchParams<{ id: string }>();
|
const { id } = useLocalSearchParams<{ id: string }>();
|
||||||
const { agents, agentStreamState, pendingPermissions, respondToPermission } = useSession();
|
const { agents, agentStreamState, pendingPermissions, respondToPermission } = useSession();
|
||||||
const { registerFooterControls, unregisterFooterControls } = useFooterControls();
|
const { registerFooterControls, unregisterFooterControls } = useFooterControls();
|
||||||
|
const [isContentReady, setIsContentReady] = useState(false);
|
||||||
|
|
||||||
// Keyboard animation
|
// Keyboard animation
|
||||||
const { height: keyboardHeight } = useReanimatedKeyboardAnimation();
|
const { height: keyboardHeight } = useReanimatedKeyboardAnimation();
|
||||||
@@ -42,8 +43,39 @@ export default function AgentScreen() {
|
|||||||
return <AgentInputArea agentId={id} />;
|
return <AgentInputArea agentId={id} />;
|
||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
||||||
|
// Defer heavy rendering to next frame for faster initial paint
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!agentControls || !agent) {
|
let isMounted = true;
|
||||||
|
let frameId: number | null = null;
|
||||||
|
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
|
const markReady = () => {
|
||||||
|
if (isMounted) {
|
||||||
|
setIsContentReady(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof requestAnimationFrame === "function") {
|
||||||
|
frameId = requestAnimationFrame(markReady);
|
||||||
|
} else {
|
||||||
|
timeoutId = setTimeout(markReady, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isMounted = false;
|
||||||
|
|
||||||
|
if (frameId !== null && typeof cancelAnimationFrame === "function") {
|
||||||
|
cancelAnimationFrame(frameId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeoutId !== null) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!agentControls || !agent || !isContentReady) {
|
||||||
unregisterFooterControls();
|
unregisterFooterControls();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -53,7 +85,7 @@ export default function AgentScreen() {
|
|||||||
return () => {
|
return () => {
|
||||||
unregisterFooterControls();
|
unregisterFooterControls();
|
||||||
};
|
};
|
||||||
}, [agentControls, agent, registerFooterControls, unregisterFooterControls]);
|
}, [agentControls, agent, isContentReady, registerFooterControls, unregisterFooterControls]);
|
||||||
|
|
||||||
if (!agent) {
|
if (!agent) {
|
||||||
return (
|
return (
|
||||||
@@ -73,20 +105,28 @@ export default function AgentScreen() {
|
|||||||
|
|
||||||
{/* Content Area with Keyboard Animation */}
|
{/* Content Area with Keyboard Animation */}
|
||||||
<ReanimatedAnimated.View style={[styles.content, animatedKeyboardStyle]}>
|
<ReanimatedAnimated.View style={[styles.content, animatedKeyboardStyle]}>
|
||||||
<AgentStreamView
|
{isContentReady ? (
|
||||||
agentId={id!}
|
<>
|
||||||
agent={agent}
|
<AgentStreamView
|
||||||
streamItems={streamItems}
|
agentId={id!}
|
||||||
pendingPermissions={agentPermissions}
|
agent={agent}
|
||||||
onPermissionResponse={(requestId, optionId) =>
|
streamItems={streamItems}
|
||||||
respondToPermission(requestId, id!, agent.sessionId || "", [optionId])
|
pendingPermissions={agentPermissions}
|
||||||
}
|
onPermissionResponse={(requestId, optionId) =>
|
||||||
/>
|
respondToPermission(requestId, id!, agent.sessionId || "", [optionId])
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Footer area - status bar pinned within screen */}
|
{/* Footer area - status bar pinned within screen */}
|
||||||
<View style={styles.footerContainer}>
|
<View style={styles.footerContainer}>
|
||||||
<AgentStatusBar agentId={id!} />
|
<AgentStatusBar agentId={id!} />
|
||||||
</View>
|
</View>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<View style={styles.loadingContainer}>
|
||||||
|
<ActivityIndicator size="large" color={theme.colors.primary} />
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
</ReanimatedAnimated.View>
|
</ReanimatedAnimated.View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
@@ -103,6 +143,11 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
footerContainer: {
|
footerContainer: {
|
||||||
backgroundColor: theme.colors.background,
|
backgroundColor: theme.colors.background,
|
||||||
},
|
},
|
||||||
|
loadingContainer: {
|
||||||
|
flex: 1,
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
},
|
||||||
errorContainer: {
|
errorContainer: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
|||||||
@@ -63,7 +63,10 @@ export function AgentList({ agents }: AgentListProps) {
|
|||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
key={agent.id}
|
key={agent.id}
|
||||||
style={styles.agentItem}
|
style={({ pressed }) => [
|
||||||
|
styles.agentItem,
|
||||||
|
pressed && styles.agentItemPressed,
|
||||||
|
]}
|
||||||
onPress={() => handleAgentPress(agent.id)}
|
onPress={() => handleAgentPress(agent.id)}
|
||||||
>
|
>
|
||||||
<View style={styles.agentContent}>
|
<View style={styles.agentContent}>
|
||||||
@@ -112,6 +115,10 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
marginBottom: theme.spacing[2],
|
marginBottom: theme.spacing[2],
|
||||||
backgroundColor: theme.colors.muted,
|
backgroundColor: theme.colors.muted,
|
||||||
},
|
},
|
||||||
|
agentItemPressed: {
|
||||||
|
opacity: 0.7,
|
||||||
|
backgroundColor: theme.colors.accent,
|
||||||
|
},
|
||||||
agentContent: {
|
agentContent: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { View, Text, Pressable, Animated } from "react-native";
|
import { View, Text, Pressable, Animated } from "react-native";
|
||||||
import { useState, useEffect, useRef } from "react";
|
import { useState, useEffect, useRef, memo } from "react";
|
||||||
import Markdown from "react-native-markdown-display";
|
import Markdown from "react-native-markdown-display";
|
||||||
import {
|
import {
|
||||||
Circle,
|
Circle,
|
||||||
@@ -48,7 +48,7 @@ const userMessageStylesheet = StyleSheet.create((theme) => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export function UserMessage({ message, timestamp }: UserMessageProps) {
|
export const UserMessage = memo(function UserMessage({ message, timestamp }: UserMessageProps) {
|
||||||
return (
|
return (
|
||||||
<View style={userMessageStylesheet.container}>
|
<View style={userMessageStylesheet.container}>
|
||||||
<View style={userMessageStylesheet.bubble}>
|
<View style={userMessageStylesheet.bubble}>
|
||||||
@@ -56,7 +56,7 @@ export function UserMessage({ message, timestamp }: UserMessageProps) {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
interface AssistantMessageProps {
|
interface AssistantMessageProps {
|
||||||
message: string;
|
message: string;
|
||||||
@@ -133,7 +133,7 @@ const assistantMessageStylesheet = StyleSheet.create((theme) => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export function AssistantMessage({
|
export const AssistantMessage = memo(function AssistantMessage({
|
||||||
message,
|
message,
|
||||||
timestamp,
|
timestamp,
|
||||||
isStreaming = false,
|
isStreaming = false,
|
||||||
@@ -191,7 +191,7 @@ export function AssistantMessage({
|
|||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
interface ActivityLogProps {
|
interface ActivityLogProps {
|
||||||
type: "system" | "info" | "success" | "error" | "artifact";
|
type: "system" | "info" | "success" | "error" | "artifact";
|
||||||
@@ -274,7 +274,7 @@ const activityLogStylesheet = StyleSheet.create((theme) => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export function ActivityLog({
|
export const ActivityLog = memo(function ActivityLog({
|
||||||
type,
|
type,
|
||||||
message,
|
message,
|
||||||
timestamp,
|
timestamp,
|
||||||
@@ -374,7 +374,7 @@ export function ActivityLog({
|
|||||||
</View>
|
</View>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
interface ToolCallProps {
|
interface ToolCallProps {
|
||||||
toolName: string;
|
toolName: string;
|
||||||
@@ -486,7 +486,7 @@ const toolKindIcons: Record<string, any> = {
|
|||||||
// Add more mappings as needed
|
// Add more mappings as needed
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ToolCall({
|
export const ToolCall = memo(function ToolCall({
|
||||||
toolName,
|
toolName,
|
||||||
kind,
|
kind,
|
||||||
args,
|
args,
|
||||||
@@ -584,4 +584,4 @@ export function ToolCall({
|
|||||||
</View>
|
</View>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createContext, useContext, useState, ReactNode, useCallback } from "react";
|
import { createContext, useContext, useState, ReactNode, useCallback, useEffect } from "react";
|
||||||
import { useSpeechmaticsAudio } from "@/hooks/use-speechmatics-audio";
|
import { useSpeechmaticsAudio } from "@/hooks/use-speechmatics-audio";
|
||||||
import { useSession } from "./session-context";
|
import { useSession } from "./session-context";
|
||||||
import { generateMessageId } from "@/types/stream";
|
import { generateMessageId } from "@/types/stream";
|
||||||
@@ -31,7 +31,7 @@ interface RealtimeProviderProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function RealtimeProvider({ children }: RealtimeProviderProps) {
|
export function RealtimeProvider({ children }: RealtimeProviderProps) {
|
||||||
const { ws, audioPlayer, isPlayingAudio, setMessages } = useSession();
|
const { ws, audioPlayer, isPlayingAudio, setMessages, setVoiceDetectionFlags } = useSession();
|
||||||
const [isRealtimeMode, setIsRealtimeMode] = useState(false);
|
const [isRealtimeMode, setIsRealtimeMode] = useState(false);
|
||||||
|
|
||||||
const realtimeAudio = useSpeechmaticsAudio({
|
const realtimeAudio = useSpeechmaticsAudio({
|
||||||
@@ -82,6 +82,11 @@ export function RealtimeProvider({ children }: RealtimeProviderProps) {
|
|||||||
detectionGracePeriod: 200,
|
detectionGracePeriod: 200,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Update voice detection flags whenever they change
|
||||||
|
useEffect(() => {
|
||||||
|
setVoiceDetectionFlags(realtimeAudio.isDetecting, realtimeAudio.isSpeaking);
|
||||||
|
}, [realtimeAudio.isDetecting, realtimeAudio.isSpeaking, setVoiceDetectionFlags]);
|
||||||
|
|
||||||
const startRealtime = useCallback(async () => {
|
const startRealtime = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
await realtimeAudio.start();
|
await realtimeAudio.start();
|
||||||
|
|||||||
@@ -87,6 +87,9 @@ interface SessionContextValue {
|
|||||||
audioPlayer: ReturnType<typeof useAudioPlayer>;
|
audioPlayer: ReturnType<typeof useAudioPlayer>;
|
||||||
isPlayingAudio: boolean;
|
isPlayingAudio: boolean;
|
||||||
setIsPlayingAudio: (playing: boolean) => void;
|
setIsPlayingAudio: (playing: boolean) => void;
|
||||||
|
|
||||||
|
// Voice detection flags (updated by RealtimeContext)
|
||||||
|
setVoiceDetectionFlags: (isDetecting: boolean, isSpeaking: boolean) => void;
|
||||||
|
|
||||||
// Messages and stream state
|
// Messages and stream state
|
||||||
messages: MessageEntry[];
|
messages: MessageEntry[];
|
||||||
@@ -133,7 +136,15 @@ interface SessionProviderProps {
|
|||||||
|
|
||||||
export function SessionProvider({ children, serverUrl }: SessionProviderProps) {
|
export function SessionProvider({ children, serverUrl }: SessionProviderProps) {
|
||||||
const ws = useWebSocket(serverUrl);
|
const ws = useWebSocket(serverUrl);
|
||||||
const audioPlayer = useAudioPlayer();
|
|
||||||
|
// State for voice detection flags (will be set by RealtimeContext)
|
||||||
|
const isDetectingRef = useRef(false);
|
||||||
|
const isSpeakingRef = useRef(false);
|
||||||
|
|
||||||
|
const audioPlayer = useAudioPlayer({
|
||||||
|
isDetecting: () => isDetectingRef.current,
|
||||||
|
isSpeaking: () => isSpeakingRef.current,
|
||||||
|
});
|
||||||
|
|
||||||
const [isPlayingAudio, setIsPlayingAudio] = useState(false);
|
const [isPlayingAudio, setIsPlayingAudio] = useState(false);
|
||||||
const [messages, setMessages] = useState<MessageEntry[]>([]);
|
const [messages, setMessages] = useState<MessageEntry[]>([]);
|
||||||
@@ -250,6 +261,20 @@ export function SessionProvider({ children, serverUrl }: SessionProviderProps) {
|
|||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Permission resolved - remove from pending
|
||||||
|
const unsubPermissionResolved = ws.on("agent_permission_resolved", (message) => {
|
||||||
|
if (message.type !== "agent_permission_resolved") return;
|
||||||
|
const { requestId, agentId, optionId } = message.payload;
|
||||||
|
|
||||||
|
console.log("[Session] Permission resolved:", requestId, "for agent:", agentId, "with option:", optionId);
|
||||||
|
|
||||||
|
setPendingPermissions((prev) => {
|
||||||
|
const next = new Map(prev);
|
||||||
|
next.delete(requestId);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Audio output
|
// Audio output
|
||||||
const unsubAudioOutput = ws.on("audio_output", async (message) => {
|
const unsubAudioOutput = ws.on("audio_output", async (message) => {
|
||||||
if (message.type !== "audio_output") return;
|
if (message.type !== "audio_output") return;
|
||||||
@@ -458,6 +483,7 @@ export function SessionProvider({ children, serverUrl }: SessionProviderProps) {
|
|||||||
unsubAgentStatus();
|
unsubAgentStatus();
|
||||||
unsubAgentUpdate();
|
unsubAgentUpdate();
|
||||||
unsubPermissionRequest();
|
unsubPermissionRequest();
|
||||||
|
unsubPermissionResolved();
|
||||||
unsubAudioOutput();
|
unsubAudioOutput();
|
||||||
unsubActivity();
|
unsubActivity();
|
||||||
unsubChunk();
|
unsubChunk();
|
||||||
@@ -581,19 +607,21 @@ export function SessionProvider({ children, serverUrl }: SessionProviderProps) {
|
|||||||
};
|
};
|
||||||
ws.send(msg);
|
ws.send(msg);
|
||||||
|
|
||||||
// Remove from pending
|
// Don't remove from pending here - wait for server confirmation via agent_permission_resolved
|
||||||
setPendingPermissions((prev) => {
|
// This ensures UI stays in sync whether permission is accepted via UI or MCP
|
||||||
const next = new Map(prev);
|
|
||||||
next.delete(requestId);
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
}, [ws]);
|
}, [ws]);
|
||||||
|
|
||||||
|
const setVoiceDetectionFlags = useCallback((isDetecting: boolean, isSpeaking: boolean) => {
|
||||||
|
isDetectingRef.current = isDetecting;
|
||||||
|
isSpeakingRef.current = isSpeaking;
|
||||||
|
}, []);
|
||||||
|
|
||||||
const value: SessionContextValue = {
|
const value: SessionContextValue = {
|
||||||
ws,
|
ws,
|
||||||
audioPlayer,
|
audioPlayer,
|
||||||
isPlayingAudio,
|
isPlayingAudio,
|
||||||
setIsPlayingAudio,
|
setIsPlayingAudio,
|
||||||
|
setVoiceDetectionFlags,
|
||||||
messages,
|
messages,
|
||||||
setMessages,
|
setMessages,
|
||||||
currentAssistantMessage,
|
currentAssistantMessage,
|
||||||
|
|||||||
@@ -42,18 +42,40 @@ function resamplePcm24kTo16k(pcm24k: Uint8Array): Uint8Array {
|
|||||||
return pcm16k;
|
return pcm16k;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AudioPlayerOptions {
|
||||||
|
isDetecting?: () => boolean;
|
||||||
|
isSpeaking?: () => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook for audio playback using Speechmatics two-way audio with echo cancellation
|
* Hook for audio playback using Speechmatics two-way audio with echo cancellation
|
||||||
*/
|
*/
|
||||||
export function useAudioPlayer() {
|
export function useAudioPlayer(options?: AudioPlayerOptions) {
|
||||||
const [isPlaying, setIsPlaying] = useState(false);
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
const [audioInitialized, setAudioInitialized] = useState(false);
|
const [audioInitialized, setAudioInitialized] = useState(false);
|
||||||
const queueRef = useRef<QueuedAudio[]>([]);
|
const queueRef = useRef<QueuedAudio[]>([]);
|
||||||
|
const suppressedQueueRef = useRef<QueuedAudio[]>([]);
|
||||||
const isProcessingQueueRef = useRef(false);
|
const isProcessingQueueRef = useRef(false);
|
||||||
const playbackTimeoutRef = useRef<number | null>(null);
|
const playbackTimeoutRef = useRef<number | null>(null);
|
||||||
|
const checkIntervalRef = useRef<number | null>(null);
|
||||||
|
|
||||||
async function play(audioData: Blob): Promise<number> {
|
async function play(audioData: Blob): Promise<number> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
// Check if we should suppress playback due to voice detection/speaking
|
||||||
|
const shouldSuppress =
|
||||||
|
(options?.isDetecting && options.isDetecting()) ||
|
||||||
|
(options?.isSpeaking && options.isSpeaking());
|
||||||
|
|
||||||
|
if (shouldSuppress) {
|
||||||
|
console.log("[AudioPlayer] Suppressing playback - voice detection/speaking active");
|
||||||
|
// Add to suppressed queue instead
|
||||||
|
suppressedQueueRef.current.push({ audioData, resolve, reject });
|
||||||
|
|
||||||
|
// Start checking for when flags clear
|
||||||
|
startCheckingForClearFlags();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Add to queue with its promise handlers
|
// Add to queue with its promise handlers
|
||||||
queueRef.current.push({ audioData, resolve, reject });
|
queueRef.current.push({ audioData, resolve, reject });
|
||||||
|
|
||||||
@@ -64,6 +86,49 @@ export function useAudioPlayer() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function startCheckingForClearFlags(): void {
|
||||||
|
// Already checking
|
||||||
|
if (checkIntervalRef.current !== null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[AudioPlayer] Starting to check for clear flags");
|
||||||
|
|
||||||
|
checkIntervalRef.current = setInterval(() => {
|
||||||
|
const isStillBlocked =
|
||||||
|
(options?.isDetecting && options.isDetecting()) ||
|
||||||
|
(options?.isSpeaking && options.isSpeaking());
|
||||||
|
|
||||||
|
if (!isStillBlocked && suppressedQueueRef.current.length > 0) {
|
||||||
|
console.log("[AudioPlayer] Flags cleared - moving suppressed queue to main queue");
|
||||||
|
|
||||||
|
// Move all suppressed items to main queue
|
||||||
|
const suppressedItems = [...suppressedQueueRef.current];
|
||||||
|
suppressedQueueRef.current = [];
|
||||||
|
|
||||||
|
// Add to front of main queue (they were waiting)
|
||||||
|
queueRef.current = [...suppressedItems, ...queueRef.current];
|
||||||
|
|
||||||
|
// Stop checking
|
||||||
|
if (checkIntervalRef.current !== null) {
|
||||||
|
clearInterval(checkIntervalRef.current);
|
||||||
|
checkIntervalRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start processing if not already
|
||||||
|
if (!isProcessingQueueRef.current) {
|
||||||
|
processQueue();
|
||||||
|
}
|
||||||
|
} else if (!isStillBlocked && suppressedQueueRef.current.length === 0) {
|
||||||
|
// No more suppressed items and flags are clear - stop checking
|
||||||
|
if (checkIntervalRef.current !== null) {
|
||||||
|
clearInterval(checkIntervalRef.current);
|
||||||
|
checkIntervalRef.current = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 100); // Check every 100ms
|
||||||
|
}
|
||||||
|
|
||||||
async function processQueue(): Promise<void> {
|
async function processQueue(): Promise<void> {
|
||||||
if (isProcessingQueueRef.current || queueRef.current.length === 0) {
|
if (isProcessingQueueRef.current || queueRef.current.length === 0) {
|
||||||
return;
|
return;
|
||||||
@@ -72,6 +137,20 @@ export function useAudioPlayer() {
|
|||||||
isProcessingQueueRef.current = true;
|
isProcessingQueueRef.current = true;
|
||||||
|
|
||||||
while (queueRef.current.length > 0) {
|
while (queueRef.current.length > 0) {
|
||||||
|
// Before processing each item, check if flags became active
|
||||||
|
const shouldSuppress =
|
||||||
|
(options?.isDetecting && options.isDetecting()) ||
|
||||||
|
(options?.isSpeaking && options.isSpeaking());
|
||||||
|
|
||||||
|
if (shouldSuppress) {
|
||||||
|
console.log("[AudioPlayer] Flags became active during processing - moving remaining queue to suppressed");
|
||||||
|
// Move remaining queue to suppressed
|
||||||
|
suppressedQueueRef.current = [...queueRef.current, ...suppressedQueueRef.current];
|
||||||
|
queueRef.current = [];
|
||||||
|
startCheckingForClearFlags();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
const item = queueRef.current.shift()!;
|
const item = queueRef.current.shift()!;
|
||||||
try {
|
try {
|
||||||
const duration = await playAudio(item.audioData);
|
const duration = await playAudio(item.audioData);
|
||||||
@@ -188,21 +267,45 @@ export function useAudioPlayer() {
|
|||||||
|
|
||||||
setIsPlaying(false);
|
setIsPlaying(false);
|
||||||
|
|
||||||
// Reject all pending promises in the queue
|
// Reject all pending promises in the main queue
|
||||||
while (queueRef.current.length > 0) {
|
while (queueRef.current.length > 0) {
|
||||||
const item = queueRef.current.shift()!;
|
const item = queueRef.current.shift()!;
|
||||||
item.reject(new Error("Playback stopped"));
|
item.reject(new Error("Playback stopped"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reject all pending promises in the suppressed queue
|
||||||
|
while (suppressedQueueRef.current.length > 0) {
|
||||||
|
const item = suppressedQueueRef.current.shift()!;
|
||||||
|
item.reject(new Error("Playback stopped"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear check interval
|
||||||
|
if (checkIntervalRef.current !== null) {
|
||||||
|
clearInterval(checkIntervalRef.current);
|
||||||
|
checkIntervalRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
isProcessingQueueRef.current = false;
|
isProcessingQueueRef.current = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearQueue(): void {
|
function clearQueue(): void {
|
||||||
// Reject all pending promises in the queue
|
// Reject all pending promises in the main queue
|
||||||
while (queueRef.current.length > 0) {
|
while (queueRef.current.length > 0) {
|
||||||
const item = queueRef.current.shift()!;
|
const item = queueRef.current.shift()!;
|
||||||
item.reject(new Error("Queue cleared"));
|
item.reject(new Error("Queue cleared"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reject all pending promises in the suppressed queue
|
||||||
|
while (suppressedQueueRef.current.length > 0) {
|
||||||
|
const item = suppressedQueueRef.current.shift()!;
|
||||||
|
item.reject(new Error("Queue cleared"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear check interval
|
||||||
|
if (checkIntervalRef.current !== null) {
|
||||||
|
clearInterval(checkIntervalRef.current);
|
||||||
|
checkIntervalRef.current = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user