fix(voice): TTS играет через AudioContext (фикс для iPad Safari)
All checks were successful
Deploy / deploy (push) Successful in 1m45s
All checks were successful
Deploy / deploy (push) Successful in 1m45s
iOS Safari блокирует <audio>.play() даже после silent-WAV unlock — каждый new Audio() считается новым элементом без gesture. Решение: при тапе кнопки в VoiceController создаём общий AudioContext (под user-gesture) и пробуждаем его. VoiceOverlay теперь играет TTS через этот ctx (decodeAudioData + BufferSource). HTMLAudioElement остаётся fallback'ом если ctx недоступен. decodeAudioData в Safari исторически callback, в Chrome — Promise: используем оба варианта.
This commit is contained in:
@@ -35,6 +35,7 @@ export default function VoiceOverlay() {
|
||||
const dismissTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
const audioRef = useRef<HTMLAudioElement | null>(null)
|
||||
const audioUrlRef = useRef<string | null>(null)
|
||||
const audioSourceRef = useRef<AudioBufferSourceNode | null>(null)
|
||||
|
||||
const clearDismiss = () => {
|
||||
if (dismissTimer.current) {
|
||||
@@ -48,6 +49,11 @@ export default function VoiceOverlay() {
|
||||
}
|
||||
|
||||
const stopAudio = () => {
|
||||
if (audioSourceRef.current) {
|
||||
try { audioSourceRef.current.stop() } catch {}
|
||||
try { audioSourceRef.current.disconnect() } catch {}
|
||||
audioSourceRef.current = null
|
||||
}
|
||||
if (audioRef.current) {
|
||||
try {
|
||||
audioRef.current.pause()
|
||||
@@ -78,6 +84,39 @@ export default function VoiceOverlay() {
|
||||
return
|
||||
}
|
||||
const blob = await r.blob()
|
||||
|
||||
// Сначала пытаемся через общий AudioContext (он разблокирован в start()
|
||||
// VoiceController при тапе пользователя). На iOS Safari это единственный
|
||||
// надёжный путь; на остальных тоже работает.
|
||||
const ctx: AudioContext | undefined = (window as any).__voicePlaybackCtx
|
||||
if (ctx) {
|
||||
try {
|
||||
if (ctx.state === 'suspended') await ctx.resume()
|
||||
const arrayBuf = await blob.arrayBuffer()
|
||||
const audioBuf: AudioBuffer = await new Promise((resolve, reject) => {
|
||||
// decodeAudioData в Safari исторически callback-API, поддерживает оба.
|
||||
try {
|
||||
const p = ctx.decodeAudioData(arrayBuf, resolve, reject) as any
|
||||
if (p && typeof p.then === 'function') p.then(resolve, reject)
|
||||
} catch (e) { reject(e) }
|
||||
})
|
||||
const source = ctx.createBufferSource()
|
||||
source.buffer = audioBuf
|
||||
source.connect(ctx.destination)
|
||||
const finish = () => {
|
||||
if (audioSourceRef.current === source) audioSourceRef.current = null
|
||||
onEnded?.()
|
||||
}
|
||||
source.onended = finish
|
||||
audioSourceRef.current = source
|
||||
source.start()
|
||||
return
|
||||
} catch (e: any) {
|
||||
console.warn('[voice] AudioContext playback failed, fallback to <audio>:', e?.message || e)
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback на HTMLAudioElement (на десктопе обычно тоже работает).
|
||||
const url = URL.createObjectURL(blob)
|
||||
audioUrlRef.current = url
|
||||
const audio = new Audio(url)
|
||||
|
||||
Reference in New Issue
Block a user