Files
sfera-new/src/components/ui/voice-recorder.tsx

254 lines
7.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client"
import { useState, useRef, useEffect } from 'react'
import { Button } from '@/components/ui/button'
import { Mic, MicOff, Square, Send, Trash2 } from 'lucide-react'
import { useAuth } from '@/hooks/useAuth'
interface VoiceRecorderProps {
onSendVoice: (audioUrl: string, duration: number) => void
}
export function VoiceRecorder({ onSendVoice }: VoiceRecorderProps) {
const { user } = useAuth()
const [isRecording, setIsRecording] = useState(false)
const [recordedAudio, setRecordedAudio] = useState<string | null>(null)
const [duration, setDuration] = useState(0)
const [isPlaying, setIsPlaying] = useState(false)
const [permission, setPermission] = useState<'granted' | 'denied' | 'prompt'>('prompt')
const mediaRecorderRef = useRef<MediaRecorder | null>(null)
const audioChunksRef = useRef<Blob[]>([])
const audioRef = useRef<HTMLAudioElement | null>(null)
const intervalRef = useRef<NodeJS.Timeout | null>(null)
useEffect(() => {
// Проверяем доступность микрофона
if (typeof navigator !== 'undefined' && navigator.mediaDevices) {
navigator.permissions.query({ name: 'microphone' as PermissionName }).then((result) => {
setPermission(result.state as 'granted' | 'denied' | 'prompt')
}).catch(() => {
// Если permissions API недоступен, оставляем prompt
})
}
}, [])
const startRecording = async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
setPermission('granted')
const mediaRecorder = new MediaRecorder(stream, {
mimeType: 'audio/webm;codecs=opus'
})
mediaRecorderRef.current = mediaRecorder
audioChunksRef.current = []
mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
audioChunksRef.current.push(event.data)
}
}
mediaRecorder.onstop = () => {
const audioBlob = new Blob(audioChunksRef.current, { type: 'audio/webm' })
const audioUrl = URL.createObjectURL(audioBlob)
setRecordedAudio(audioUrl)
// Останавливаем все треки для освобождения микрофона
stream.getTracks().forEach(track => track.stop())
// Останавливаем таймер
if (intervalRef.current) {
clearInterval(intervalRef.current)
intervalRef.current = null
}
}
mediaRecorder.start()
setIsRecording(true)
setDuration(0)
// Запускаем таймер записи
intervalRef.current = setInterval(() => {
setDuration(prev => prev + 1)
}, 1000)
} catch (error) {
console.error('Error accessing microphone:', error)
setPermission('denied')
}
}
const stopRecording = () => {
if (mediaRecorderRef.current && isRecording) {
mediaRecorderRef.current.stop()
setIsRecording(false)
}
}
const cancelRecording = () => {
if (mediaRecorderRef.current && isRecording) {
mediaRecorderRef.current.stop()
setIsRecording(false)
}
setRecordedAudio(null)
setDuration(0)
if (intervalRef.current) {
clearInterval(intervalRef.current)
intervalRef.current = null
}
}
const playRecording = () => {
if (recordedAudio) {
if (audioRef.current) {
audioRef.current.pause()
audioRef.current.currentTime = 0
}
audioRef.current = new Audio(recordedAudio)
audioRef.current.play()
setIsPlaying(true)
audioRef.current.onended = () => {
setIsPlaying(false)
}
}
}
const sendVoiceMessage = async () => {
if (!recordedAudio || !user?.id) return
try {
// Конвертируем Blob в File для загрузки
const response = await fetch(recordedAudio)
const blob = await response.blob()
const file = new File([blob], `voice-${Date.now()}.webm`, { type: 'audio/webm' })
// Загружаем в S3
const formData = new FormData()
formData.append('file', file)
formData.append('userId', user.id)
const uploadResponse = await fetch('/api/upload-voice', {
method: 'POST',
body: formData
})
if (!uploadResponse.ok) {
throw new Error('Failed to upload voice message')
}
const result = await uploadResponse.json()
// Отправляем голосовое сообщение
onSendVoice(result.url, duration)
// Очищаем состояние
setRecordedAudio(null)
setDuration(0)
} catch (error) {
console.error('Error sending voice message:', error)
}
}
const formatDuration = (seconds: number) => {
const mins = Math.floor(seconds / 60)
const secs = seconds % 60
return `${mins}:${secs.toString().padStart(2, '0')}`
}
if (permission === 'denied') {
return (
<div className="text-white/60 text-xs text-center p-2">
Доступ к микрофону запрещен
</div>
)
}
return (
<div className="flex items-center space-x-2">
{!recordedAudio ? (
// Состояние записи
<>
{!isRecording ? (
<Button
type="button"
variant="ghost"
size="sm"
onClick={startRecording}
className="text-white/60 hover:text-white hover:bg-white/10 p-2"
>
<Mic className="h-4 w-4" />
</Button>
) : (
<>
<div className="flex items-center space-x-2 bg-red-500/20 rounded-lg px-3 py-2">
<div className="w-2 h-2 bg-red-500 rounded-full animate-pulse" />
<span className="text-white text-sm font-mono">
{formatDuration(duration)}
</span>
<Button
type="button"
variant="ghost"
size="sm"
onClick={stopRecording}
className="text-white/80 hover:text-white p-1"
>
<Square className="h-3 w-3" />
</Button>
<Button
type="button"
variant="ghost"
size="sm"
onClick={cancelRecording}
className="text-red-400 hover:text-red-300 p-1"
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
</>
)}
</>
) : (
// Состояние воспроизведения и отправки
<div className="flex items-center space-x-2 bg-blue-500/20 rounded-lg px-3 py-2">
<Button
type="button"
variant="ghost"
size="sm"
onClick={playRecording}
className="text-blue-300 hover:text-blue-200 p-1"
>
{isPlaying ? <MicOff className="h-3 w-3" /> : <Mic className="h-3 w-3" />}
</Button>
<span className="text-white text-sm font-mono">
{formatDuration(duration)}
</span>
<Button
type="button"
variant="ghost"
size="sm"
onClick={sendVoiceMessage}
className="text-green-400 hover:text-green-300 p-1"
>
<Send className="h-3 w-3" />
</Button>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => setRecordedAudio(null)}
className="text-red-400 hover:text-red-300 p-1"
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
)}
</div>
)
}