// `SpeechRecognition` is an API used on the browser so we can safely disable // the `window` check. /* eslint-disable no-restricted-globals */ /* global SpeechRecognition SpeechRecognitionEvent */ import type { CreateVoiceSearchHelper, Status, VoiceListeningState, } from './types'; const createVoiceSearchHelper: CreateVoiceSearchHelper = function createVoiceSearchHelper({ searchAsYouSpeak, language, onQueryChange, onStateChange, }) { const SpeechRecognitionAPI: new () => SpeechRecognition = (window as any).webkitSpeechRecognition || (window as any).SpeechRecognition; const getDefaultState = (status: Status): VoiceListeningState => ({ status, transcript: '', isSpeechFinal: false, errorCode: undefined, }); let state: VoiceListeningState = getDefaultState('initial'); let recognition: SpeechRecognition | undefined; const isBrowserSupported = (): boolean => Boolean(SpeechRecognitionAPI); const isListening = (): boolean => state.status === 'askingPermission' || state.status === 'waiting' || state.status === 'recognizing'; const setState = (newState: Partial = {}): void => { state = { ...state, ...newState }; onStateChange(); }; const getState = (): VoiceListeningState => state; const resetState = (status: Status = 'initial'): void => { setState(getDefaultState(status)); }; const onStart = (): void => { setState({ status: 'waiting', }); }; const onError = (event: Event): void => { setState({ status: 'error', errorCode: (event as any).error }); }; const onResult = (event: SpeechRecognitionEvent): void => { setState({ status: 'recognizing', transcript: (event.results[0] && event.results[0][0] && event.results[0][0].transcript) || '', isSpeechFinal: event.results[0] && event.results[0].isFinal, }); if (searchAsYouSpeak && state.transcript) { onQueryChange(state.transcript); } }; const onEnd = (): void => { if (!state.errorCode && state.transcript && !searchAsYouSpeak) { onQueryChange(state.transcript); } if (state.status !== 'error') { setState({ status: 'finished' }); } }; const startListening = (): void => { recognition = new SpeechRecognitionAPI(); if (!recognition) { return; } resetState('askingPermission'); recognition.interimResults = true; if (language) { recognition.lang = language; } recognition.addEventListener('start', onStart); recognition.addEventListener('error', onError); recognition.addEventListener('result', onResult); recognition.addEventListener('end', onEnd); recognition.start(); }; const dispose = (): void => { if (!recognition) { return; } recognition.stop(); recognition.removeEventListener('start', onStart); recognition.removeEventListener('error', onError); recognition.removeEventListener('result', onResult); recognition.removeEventListener('end', onEnd); recognition = undefined; }; const stopListening = (): void => { dispose(); // Because `dispose` removes event listeners, `end` listener is not called. // So we're setting the `status` as `finished` here. // If we don't do it, it will be still `waiting` or `recognizing`. resetState('finished'); }; return { getState, isBrowserSupported, isListening, startListening, stopListening, dispose, }; }; export default createVoiceSearchHelper;