@supunlakmal/hooks
Version:
A collection of reusable React hooks
101 lines • 4.6 kB
JavaScript
import { useState, useEffect, useCallback, useRef } from 'react';
const isBrowser = typeof window !== 'undefined';
const synth = isBrowser ? window.speechSynthesis : undefined;
/**
* Hook to utilize the browser's Speech Synthesis API (Text-to-Speech).
* Provides controls to speak text, cancel speech, list available voices, and track status.
*
* @returns An object with speech synthesis state and control functions.
*/
export function useSpeechSynthesis() {
const [voices, setVoices] = useState([]);
const [speaking, setSpeaking] = useState(false);
const [error, setError] = useState(null);
const isSupported = !!synth;
// Ref to hold the utterance instance to prevent stale closures in callbacks
const utteranceRef = useRef(null);
const updateVoices = useCallback(() => {
if (isSupported) {
try {
const availableVoices = (synth === null || synth === void 0 ? void 0 : synth.getVoices()) || [];
setVoices(availableVoices);
setError(null); // Clear previous error if voices load now
}
catch (err) {
console.error('Error getting voices:', err);
setError(err instanceof Error ? err : new Error('Failed to get voices'));
}
}
}, [isSupported]);
// Load voices initially and update when the voices list changes
useEffect(() => {
if (!isSupported)
return;
updateVoices(); // Initial attempt
// Voices might load asynchronously, listen for changes
synth === null || synth === void 0 ? void 0 : synth.addEventListener('voiceschanged', updateVoices);
return () => {
synth === null || synth === void 0 ? void 0 : synth.removeEventListener('voiceschanged', updateVoices);
// Cancel any ongoing speech on unmount
if (utteranceRef.current) {
synth === null || synth === void 0 ? void 0 : synth.cancel();
}
};
}, [isSupported, updateVoices]);
const speak = useCallback((text, options = {}) => {
var _a, _b, _c, _d;
if (!isSupported || speaking)
return; // Not supported or already speaking
// Cancel previous utterance if any (shouldn't be needed if speaking state is accurate, but safe)
synth === null || synth === void 0 ? void 0 : synth.cancel();
const utterance = new SpeechSynthesisUtterance(text);
utteranceRef.current = utterance; // Store ref
// Apply options
utterance.voice = options.voice || null;
utterance.lang = options.lang || ((_a = voices[0]) === null || _a === void 0 ? void 0 : _a.lang) || 'en-US'; // Default lang
utterance.pitch = (_b = options.pitch) !== null && _b !== void 0 ? _b : 1;
utterance.rate = (_c = options.rate) !== null && _c !== void 0 ? _c : 1;
utterance.volume = (_d = options.volume) !== null && _d !== void 0 ? _d : 1;
utterance.onstart = () => {
setSpeaking(true);
setError(null);
};
utterance.onend = () => {
setSpeaking(false);
utteranceRef.current = null;
};
utterance.onerror = (event) => {
console.error('Speech synthesis error:', event.error);
setError(new Error(`Speech error: ${event.error}`));
setSpeaking(false);
utteranceRef.current = null;
};
try {
synth === null || synth === void 0 ? void 0 : synth.speak(utterance);
}
catch (err) {
console.error('Error calling synth.speak:', err);
setError(err instanceof Error ? err : new Error('Failed to initiate speech'));
setSpeaking(false); // Ensure speaking is false if speak fails immediately
utteranceRef.current = null;
}
}, [isSupported, speaking, voices]); // voices needed for default lang
const cancel = useCallback(() => {
if (!isSupported || !speaking)
return;
synth === null || synth === void 0 ? void 0 : synth.cancel();
// Note: onend will fire after cancel, which sets speaking to false.
// Explicitly setting here might cause a race condition if onend hasn't fired yet.
// Relying on the onend handler is generally safer.
// setSpeaking(false); // Let onend handle this
}, [isSupported, speaking]);
return {
isSupported,
voices,
speaking,
speak,
cancel,
error,
};
}
//# sourceMappingURL=useSpeechSynthesis.js.map