import * as React from 'react'; import { AudioEncoderAndroidType, AudioSet, AudioSourceAndroidType, AVEncoderAudioQualityIOSType, AVEncodingOption, AVModeIOSOption, } from 'react-native-audio-recorder-player'; import { useChatContext } from '../../chat'; import { uilog } from '../../const'; import { Services } from '../../services'; import type { TimerTextRef } from '../../ui/Text'; import { getFileExtension, LocalPath, uuid } from '../../utils'; import type { VoiceBarProps, VoiceBarState } from './types'; export function useVoiceBar(props: VoiceBarProps) { const { onClickedRecordButton, onClickedClearButton, onClickedSendButton, onFailed, onState: propsOnState, } = props; const [state, setState] = React.useState('idle'); const voiceFilePathRef = React.useRef(''); const voiceFileNameRef = React.useRef(''); const voiceDurationRef = React.useRef(0); const isPlayingRef = React.useRef(false); const recordTimeoutRef = React.useRef(undefined); const im = useChatContext(); const contentTimerRef = React.useRef({} as any); const [playRipple, setPlayRipple] = React.useState(false); const AudioOptionRef = React.useRef({ AudioEncoderAndroid: AudioEncoderAndroidType.AAC, // OutputFormatAndroid: OutputFormatAndroidType.AAC_ADIF, AudioSourceAndroid: AudioSourceAndroidType.MIC, AVModeIOS: AVModeIOSOption.measurement, AVEncoderAudioQualityKeyIOS: AVEncoderAudioQualityIOSType.high, AVNumberOfChannelsKeyIOS: 2, AVFormatIDKeyIOS: AVEncodingOption.aac, // !!! amr is not supported }); const [currentTime, setCurrentTime] = React.useState(0); const onContentTimeChanged = React.useCallback((v: number) => { setCurrentTime(v); }, []); const onState = React.useCallback( (s: VoiceBarState) => { propsOnState?.(s); setState(s); }, [propsOnState] ); const startRecord = (voiceFilePath?: string) => { if (voiceFilePath) { voiceFilePathRef.current = voiceFilePath; } onState('recording'); recordTimeoutRef.current = setTimeout(() => { stopRecord(); }, 60000); contentTimerRef.current?.reset?.(); contentTimerRef.current?.start?.(); setPlayRipple(true); Services.ms .startRecordAudio({ url: voiceFilePath, audio: AudioOptionRef.current, onPosition: (pos) => { uilog.log('startRecordAudio:pos:', pos); voiceDurationRef.current = Math.floor(pos); }, onFailed: (error) => { uilog.warn('startRecordAudio:onFailed:', error); onFailed?.({ reason: 'record voice is failed.', error: error }); contentTimerRef.current?.stop?.(); setPlayRipple(false); }, onFinished: ({ result, path, error }) => { uilog.log('startRecordAudio:onFinished:', result, path, error); }, }) .then((result) => { uilog.log('startRecordAudio:result:', result); }) .catch((error) => { uilog.warn('startRecordAudio:error:', error); onFailed?.({ reason: 'record voice is failed.', error: error }); contentTimerRef.current?.stop?.(); setPlayRipple(false); }); }; const stopRecord = React.useCallback(() => { contentTimerRef.current?.stop?.(); if (recordTimeoutRef.current) { clearTimeout(recordTimeoutRef.current); recordTimeoutRef.current = undefined; } onState('stopping'); setPlayRipple(false); const conv = im.getCurrentConversation(); if (!conv) { Services.ms.stopRecordAudio(); return; } Services.ms .stopRecordAudio() .then((result?: { pos: number; path: string }) => { if (result?.path) { voiceFileNameRef.current = uuid(); let localPath = LocalPath.sendVoice( Services.dcs.getFileDir(conv.convId, voiceFileNameRef.current) ); const extension = getFileExtension(result.path); localPath = localPath + extension; voiceFilePathRef.current = localPath; voiceFileNameRef.current = voiceFileNameRef.current + extension; Services.ms .saveFromLocal({ targetPath: localPath, localPath: result.path, }) .catch((error) => { onFailed?.({ reason: 'save file voice is failed.', error: error, }); }); } }) .catch((error) => { onFailed?.({ reason: 'stop record voice is failed.', error: error, }); }); }, [im, onFailed, onState]); const replay = async () => { onState('playing'); if (isPlayingRef.current === true) { return; } isPlayingRef.current = true; contentTimerRef.current?.reset?.(); contentTimerRef.current?.start?.(); setPlayRipple(true); Services.ms .playAudio({ url: LocalPath.playVoice(voiceFilePathRef.current), onPlay({ currentPosition, duration }) { if (currentPosition === duration) { isPlayingRef.current = false; contentTimerRef.current?.stop?.(); onState('stopping'); setPlayRipple(false); } }, }) .then(() => {}) .catch((error) => { onFailed?.({ reason: 'play voice is failed.', error: error }); isPlayingRef.current = false; contentTimerRef.current?.stop?.(); onState('stopping'); setPlayRipple(false); }); }; const _onClickedRecordButton = () => { onClickedRecordButton?.(state); switch (state) { case 'idle': startRecord(); break; case 'recording': stopRecord(); break; case 'playing': replay(); break; case 'stopping': replay(); break; } }; const _onClickedClearButton = () => { onClickedClearButton?.(); onState('idle'); setCurrentTime(0); if (isPlayingRef.current === true) { Services.ms.stopAudio(); isPlayingRef.current = false; } if (recordTimeoutRef.current) { clearTimeout(recordTimeoutRef.current); recordTimeoutRef.current = undefined; } contentTimerRef.current?.stop?.(); voiceFilePathRef.current = ''; }; const _onClickedSendButton = () => { onClickedSendButton?.({ localPath: voiceFilePathRef.current, duration: voiceDurationRef.current, displayName: voiceFileNameRef.current, type: 'voice', }); }; React.useEffect(() => { return () => { if (recordTimeoutRef.current) { clearTimeout(recordTimeoutRef.current); recordTimeoutRef.current = undefined; stopRecord(); } if (isPlayingRef.current === true) { Services.ms.stopAudio(); isPlayingRef.current = false; } }; }, [stopRecord]); return { state, onClickedRecordButton: _onClickedRecordButton, onClickedClearButton: _onClickedClearButton, onClickedSendButton: _onClickedSendButton, contentTimerRef, playRipple, onContentTimeChanged, currentTime: currentTime, }; }