diff --git a/src/store/useSpeechService.ts b/src/store/useSpeechService.ts index 3f5ad44..38421e1 100644 --- a/src/store/useSpeechService.ts +++ b/src/store/useSpeechService.ts @@ -24,6 +24,7 @@ export interface SpeechSettings { combineGiftDelay: number | undefined azureVoice: string azureLanguage: string + outputDeviceId: string } export interface SpeechInfo { @@ -69,6 +70,7 @@ const DEFAULT_SETTINGS: SpeechSettings = { combineGiftDelay: 2, azureVoice: 'zh-CN-XiaoxiaoNeural', azureLanguage: 'zh-CN', + outputDeviceId: 'default', } export const templateConstants = { @@ -433,11 +435,11 @@ function createSpeechService() { speechState.isSpeaking = true speechState.isApiAudioLoading = true - + // 先清空 apiAudioSrc,确保 audio 元素能够正确重新加载 // 这样可以避免连续播放时 src 更新不触发加载的问题 speechState.apiAudioSrc = '' - + // 使用 nextTick 确保 DOM 更新后再设置新的 src // 但由于这是在 store 中,我们使用 setTimeout 来模拟 setTimeout(() => { diff --git a/src/views/open_live/ReadDanmaku.vue b/src/views/open_live/ReadDanmaku.vue index 30c9aec..0d53a5b 100644 --- a/src/views/open_live/ReadDanmaku.vue +++ b/src/views/open_live/ReadDanmaku.vue @@ -73,6 +73,10 @@ const { const azureVoices = ref>([]) const azureVoicesLoading = ref(false) +// 音频输出设备列表 +const audioOutputDevices = ref>([]) +const audioOutputDevicesLoading = ref(false) + // 计算属性 const isVtsuruVoiceAPI = computed(() => { return ( @@ -290,6 +294,58 @@ function onAudioError(e: Event) { onAPIError(e) } +/** + * 获取音频输出设备列表 + */ +async function fetchAudioOutputDevices() { + audioOutputDevicesLoading.value = true + try { + if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) { + message.warning('当前浏览器不支持设备枚举') + return + } + + const devices = await navigator.mediaDevices.enumerateDevices() + const outputDevices = devices.filter(device => device.kind === 'audiooutput') + + audioOutputDevices.value = [ + { label: '默认设备', value: 'default' }, + ...outputDevices.map(device => ({ + label: device.label || `设备 ${device.deviceId.substring(0, 8)}`, + value: device.deviceId, + })), + ] + + console.log('[TTS] 音频输出设备列表:', audioOutputDevices.value) + } catch (error) { + console.error('[TTS] 获取音频输出设备失败:', error) + message.error('获取音频输出设备失败,可能需要授予麦克风权限') + } finally { + audioOutputDevicesLoading.value = false + } +} + +/** + * 设置音频元素的输出设备 + */ +async function setAudioOutputDevice() { + if (!apiAudio.value || !settings.value.outputDeviceId) { + return + } + + try { + if (typeof apiAudio.value.setSinkId === 'function') { + await apiAudio.value.setSinkId(settings.value.outputDeviceId) + console.log(`[TTS] 已切换到输出设备: ${settings.value.outputDeviceId}`) + } else { + console.warn('[TTS] 当前浏览器不支持选择输出设备') + } + } catch (error) { + console.error('[TTS] 设置输出设备失败:', error) + message.error('设置输出设备失败') + } +} + // 生命周期 onMounted(async () => { await speechService.initialize() @@ -304,6 +360,14 @@ onMounted(async () => { if (settings.value.voiceType === 'azure') { fetchAzureVoices() } + + // 获取音频输出设备列表 + await fetchAudioOutputDevices() + + // 监听输出设备变化 + if (navigator.mediaDevices) { + navigator.mediaDevices.addEventListener('devicechange', fetchAudioOutputDevices) + } }) onUnmounted(() => { @@ -314,6 +378,11 @@ onUnmounted(() => { client.offEvent('enter', onGetEvent) speechService.stopSpeech() + + // 移除设备变化监听器 + if (navigator.mediaDevices) { + navigator.mediaDevices.removeEventListener('devicechange', fetchAudioOutputDevices) + } }) @@ -702,6 +771,47 @@ onUnmounted(() => { vertical :size="16" > + +
+ + 输出设备 + + 加载设备列表 + + + + + + 未检测到其他音频设备。某些浏览器需要授予麦克风权限才能列出所有设备。 + +
+ + + { @ended="cancelSpeech" @canplay="onAudioCanPlay" @error="onAudioError" + @loadedmetadata="setAudioOutputDevice" />