From 70e431eebc4a28d1c8a991f7b0ae9bd65e4957cf Mon Sep 17 00:00:00 2001 From: wang <3202024218@qq.com> Date: Wed, 25 Jun 2025 00:23:27 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=97=A0=E6=B3=95=E6=92=AD?= =?UTF-8?q?=E6=94=BEbug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- goldminer/backend/app.py | 26 ++- .../node_modules/.cache/eslint/123ee647.json | 2 +- .../frontend/src/components/ChatRoom.vue | 210 +++++++++++------- 3 files changed, 154 insertions(+), 84 deletions(-) diff --git a/goldminer/backend/app.py b/goldminer/backend/app.py index addddfdd..1622ebc5 100644 --- a/goldminer/backend/app.py +++ b/goldminer/backend/app.py @@ -675,16 +675,33 @@ def handle_send_audio_message(data): room_id = data.get('room_id') audio_data = data.get('audio_data') # Base64编码的音频数据 audio_duration = data.get('audio_duration', 0) # 音频持续时间(秒) + audio_mime_type = data.get('audio_mime_type', 'audio/webm') # 音频MIME类型 if not room_id or not audio_data: logger.error("语音消息缺少必要参数: room_id或audio_data") return - # 检查audio_data是否为有效的base64编码数据 - if not audio_data.startswith('data:audio'): - logger.warning(f"接收到非标准格式的音频数据,前缀: {audio_data[:20]}") + # 检查和修复音频数据格式 + try: + # 确保音频数据有正确的MIME类型前缀 + if not audio_data.startswith('data:audio'): + logger.warning(f"接收到非标准格式的音频数据,尝试修复") + + # 如果数据是base64但没有前缀,添加前缀 + if not audio_data.startswith('data:'): + # 从base64数据中移除可能存在的前缀 + cleaned_data = audio_data.replace('data:audio/webm;base64,', '') + cleaned_data = cleaned_data.replace('data:audio/ogg;base64,', '') + cleaned_data = cleaned_data.replace('data:audio/wav;base64,', '') + cleaned_data = cleaned_data.replace('data:;base64,', '') + + # 使用客户端提供的MIME类型 + audio_data = f'data:{audio_mime_type};base64,{cleaned_data}' + logger.info(f"已修复音频数据格式,使用MIME类型: {audio_mime_type}") + except Exception as e: + logger.error(f"修复音频数据格式时出错: {str(e)}") - logger.info(f"处理语音消息: 用户ID={user_id}, 房间ID={room_id}, 音频时长={audio_duration}秒, 数据长度={len(audio_data)}") + logger.info(f"处理语音消息: 用户ID={user_id}, 房间ID={room_id}, 音频时长={audio_duration}秒, MIME类型={audio_mime_type}, 数据长度={len(audio_data)}") try: # 获取用户和房间信息 @@ -716,6 +733,7 @@ def handle_send_audio_message(data): 'message': audio_data, 'message_type': 'audio', 'audio_duration': audio_duration, + 'audio_mime_type': audio_mime_type, 'created_at': message.created_at.isoformat() }, room=str(room_id)) logger.info(f"语音消息已广播到房间: {room_id}") diff --git a/goldminer/frontend/node_modules/.cache/eslint/123ee647.json b/goldminer/frontend/node_modules/.cache/eslint/123ee647.json index 6f107444..e868a7aa 100644 --- a/goldminer/frontend/node_modules/.cache/eslint/123ee647.json +++ b/goldminer/frontend/node_modules/.cache/eslint/123ee647.json @@ -1 +1 @@ -[{"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\main.js":"1","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\App.vue":"2","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\router.js":"3","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Home.vue":"4","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Register.vue":"5","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Game.vue":"6","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Login.vue":"7","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\UserProfile.vue":"8","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\ChatRoom.vue":"9","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Leaderboard.vue":"10","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Header.vue":"11","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Admin.vue":"12","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\AdminSetup.vue":"13"},{"size":2697,"mtime":1750769523218,"results":"14","hashOfConfig":"15"},{"size":1004,"mtime":1750128639386,"results":"16","hashOfConfig":"15"},{"size":2623,"mtime":1750303858493,"results":"17","hashOfConfig":"15"},{"size":5735,"mtime":1750209720346,"results":"18","hashOfConfig":"15"},{"size":4492,"mtime":1750147385471,"results":"19","hashOfConfig":"15"},{"size":53697,"mtime":1750767242119,"results":"20","hashOfConfig":"15"},{"size":7100,"mtime":1750769571067,"results":"21","hashOfConfig":"15"},{"size":11652,"mtime":1750381631833,"results":"22","hashOfConfig":"15"},{"size":59225,"mtime":1750770950073,"results":"23","hashOfConfig":"15"},{"size":13225,"mtime":1750128639387,"results":"24","hashOfConfig":"15"},{"size":4886,"mtime":1750381631832,"results":"25","hashOfConfig":"15"},{"size":19436,"mtime":1750391010410,"results":"26","hashOfConfig":"15"},{"size":4836,"mtime":1750303858491,"results":"27","hashOfConfig":"15"},{"filePath":"28","messages":"29","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"30"},"i331ru",{"filePath":"31","messages":"32","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"33"},{"filePath":"34","messages":"35","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"30"},{"filePath":"36","messages":"37","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"33"},{"filePath":"38","messages":"39","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"33"},{"filePath":"40","messages":"41","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"33"},{"filePath":"42","messages":"43","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"33"},{"filePath":"44","messages":"45","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"33"},{"filePath":"46","messages":"47","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"48","messages":"49","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"33"},{"filePath":"50","messages":"51","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"33"},{"filePath":"52","messages":"53","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"33"},{"filePath":"54","messages":"55","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"33"},"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\main.js",[],[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\App.vue",[],[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\router.js",[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Home.vue",[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Register.vue",[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Game.vue",[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Login.vue",[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\UserProfile.vue",[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\ChatRoom.vue",[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Leaderboard.vue",[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Header.vue",[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Admin.vue",[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\AdminSetup.vue",[]] \ No newline at end of file +[{"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\main.js":"1","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\App.vue":"2","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\router.js":"3","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Home.vue":"4","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Register.vue":"5","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Game.vue":"6","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Login.vue":"7","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\UserProfile.vue":"8","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\ChatRoom.vue":"9","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Leaderboard.vue":"10","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Header.vue":"11","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Admin.vue":"12","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\AdminSetup.vue":"13"},{"size":2697,"mtime":1750769523218,"results":"14","hashOfConfig":"15"},{"size":1004,"mtime":1750128639386,"results":"16","hashOfConfig":"15"},{"size":2623,"mtime":1750303858493,"results":"17","hashOfConfig":"15"},{"size":5735,"mtime":1750209720346,"results":"18","hashOfConfig":"15"},{"size":4492,"mtime":1750147385471,"results":"19","hashOfConfig":"15"},{"size":53697,"mtime":1750767242119,"results":"20","hashOfConfig":"15"},{"size":7100,"mtime":1750769571067,"results":"21","hashOfConfig":"15"},{"size":11652,"mtime":1750381631833,"results":"22","hashOfConfig":"15"},{"size":61600,"mtime":1750781985794,"results":"23","hashOfConfig":"15"},{"size":13225,"mtime":1750128639387,"results":"24","hashOfConfig":"15"},{"size":4886,"mtime":1750381631832,"results":"25","hashOfConfig":"15"},{"size":19436,"mtime":1750391010410,"results":"26","hashOfConfig":"15"},{"size":4836,"mtime":1750303858491,"results":"27","hashOfConfig":"15"},{"filePath":"28","messages":"29","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"30"},"i331ru",{"filePath":"31","messages":"32","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"33"},{"filePath":"34","messages":"35","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"30"},{"filePath":"36","messages":"37","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"33"},{"filePath":"38","messages":"39","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"33"},{"filePath":"40","messages":"41","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"33"},{"filePath":"42","messages":"43","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"33"},{"filePath":"44","messages":"45","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"33"},{"filePath":"46","messages":"47","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"48","messages":"49","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"33"},{"filePath":"50","messages":"51","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"33"},{"filePath":"52","messages":"53","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"33"},{"filePath":"54","messages":"55","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"33"},"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\main.js",[],[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\App.vue",[],[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\router.js",[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Home.vue",[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Register.vue",[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Game.vue",[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Login.vue",[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\UserProfile.vue",[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\ChatRoom.vue",[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Leaderboard.vue",[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Header.vue",[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Admin.vue",[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\AdminSetup.vue",[]] \ No newline at end of file diff --git a/goldminer/frontend/src/components/ChatRoom.vue b/goldminer/frontend/src/components/ChatRoom.vue index 1cca7fae..be02ac88 100644 --- a/goldminer/frontend/src/components/ChatRoom.vue +++ b/goldminer/frontend/src/components/ChatRoom.vue @@ -194,16 +194,14 @@ /> - + @@ -480,59 +478,8 @@ export default { } }) - // WebRTC语音聊天事件 - socket.on('voice_chat_event', (data) => { - console.log('收到语音聊天事件:', data) - - const { type, user_id, username } = data - - switch (type) { - case 'user_joined': { - // 有新用户加入语音聊天 - addNotification(`${username} 加入了语音聊天`) - - // 将用户添加到参与者列表 - if (!rtcParticipants.value.find(p => p.id === user_id)) { - rtcParticipants.value.push({ - id: user_id, - username: username, - muted: false - }) - } - - // 如果自己在语音聊天中,发起连接 - if (rtcActive.value && localStream.value) { - // 通知新用户有新的对等连接 - sendSignal(user_id, 'new_peer', {}) - } - break - } - - case 'user_left': { - // 用户离开语音聊天 - addNotification(`${username} 离开了语音聊天`) - - // 从参与者列表中移除 - rtcParticipants.value = rtcParticipants.value.filter(p => p.id !== user_id) - - // 关闭与该用户的对等连接 - closePeerConnection(user_id) - break - } - - case 'mute_change': { - // 用户改变静音状态 - const mutedStatus = data.muted - const participant = rtcParticipants.value.find(p => p.id === user_id) - - if (participant) { - participant.muted = mutedStatus - addNotification(`${username} ${mutedStatus ? '已静音' : '已取消静音'}`) - } - break - } - } - }) + + } // 获取聊天室列表 @@ -646,10 +593,37 @@ export default { } }) - // 使用更高效的编码配置 - const options = { - mimeType: 'audio/webm;codecs=opus', - audioBitsPerSecond: 32000 // 降低比特率以减小文件大小 + // 检测浏览器支持的音频格式 + let options = {}; + let mimeType = ''; + + // 按优先级检测支持的格式 + if (MediaRecorder.isTypeSupported('audio/webm;codecs=opus')) { + mimeType = 'audio/webm;codecs=opus'; + console.log('使用 audio/webm;codecs=opus 格式录音'); + } + else if (MediaRecorder.isTypeSupported('audio/webm')) { + mimeType = 'audio/webm'; + console.log('使用 audio/webm 格式录音'); + } + else if (MediaRecorder.isTypeSupported('audio/ogg;codecs=opus')) { + mimeType = 'audio/ogg;codecs=opus'; + console.log('使用 audio/ogg;codecs=opus 格式录音'); + } + else if (MediaRecorder.isTypeSupported('audio/mp4')) { + mimeType = 'audio/mp4'; + console.log('使用 audio/mp4 格式录音'); + } + else { + console.log('使用默认音频格式录音'); + } + + // 设置录音选项 + if (mimeType) { + options = { + mimeType: mimeType, + audioBitsPerSecond: 32000 // 降低比特率以减小文件大小 + }; } // 创建媒体录制器 @@ -682,7 +656,7 @@ export default { console.log('录音实际时长:', actualDuration, '秒') // 将录制的音频块转换为Blob - const audioBlob = new Blob(audioChunks.value, { type: 'audio/webm' }) + const audioBlob = new Blob(audioChunks.value, { type: mimeType || 'audio/webm' }) // 添加本地预览消息,减少感知延迟 const tempId = 'temp-' + Date.now() @@ -706,12 +680,14 @@ export default { reader.readAsDataURL(audioBlob) reader.onloadend = () => { const base64Audio = reader.result + console.log('音频格式:', mimeType || 'unknown', '数据前缀:', base64Audio.substring(0, 50)) // 发送语音消息 socket.emit('send_audio_message', { room_id: currentRoom.value.id, audio_data: base64Audio, - audio_duration: actualDuration + audio_duration: actualDuration, + audio_mime_type: mimeType || 'audio/webm' }) // 重置录音状态 @@ -744,6 +720,15 @@ export default { } } + // 切换录音状态 + const toggleRecording = () => { + if (isRecording.value) { + stopRecording() + } else { + startRecording() + } + } + // 停止录音 const stopRecording = () => { if (!mediaRecorder.value || mediaRecorder.value.state === 'inactive') { @@ -799,24 +784,53 @@ export default { console.log('准备播放语音消息:', message.id, '类型:', typeof message.message); // 确保音频源是有效的 + let audioSource = null; + const mimeType = message.audio_mime_type || 'audio/webm'; + if (message.localAudioUrl && message.localAudioUrl.startsWith('blob:')) { // 优先使用本地缓存的Blob URL - audioPlayer.value.src = message.localAudioUrl; + audioSource = message.localAudioUrl; console.log('使用本地缓存的Blob URL播放'); } else if (message.message.startsWith('blob:')) { // 本地消息使用Blob URL - audioPlayer.value.src = message.message; + audioSource = message.message; console.log('使用Blob URL播放:', message.message); } else if (message.message.startsWith('data:audio')) { // 服务器返回的base64音频数据 - audioPlayer.value.src = message.message; + audioSource = message.message; console.log('使用base64数据播放'); } else { // 尝试将消息转换为有效的音频源 - console.warn('未知格式的音频消息,尝试直接使用:', message.message.substring(0, 50) + '...'); - audioPlayer.value.src = message.message; + console.warn('未知格式的音频消息,尝试修复格式'); + + // 尝试修复可能的格式问题 + try { + // 检查是否是有效的base64数据但缺少头部 + if (typeof message.message === 'string') { + // 尝试检测是否为base64编码 + const base64Regex = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/; + const cleanedData = message.message.replace(/^data:.*?;base64,/, ''); + + if (base64Regex.test(cleanedData)) { + // 添加适当的MIME类型头 + audioSource = `data:${mimeType};base64,` + cleanedData; + console.log(`修复为带头部的base64数据,使用MIME类型: ${mimeType}`); + } else { + // 可能不是base64,直接使用 + audioSource = message.message; + } + } else { + audioSource = message.message; + } + } catch (formatErr) { + console.error('修复音频格式失败:', formatErr); + audioSource = message.message; + } } + // 设置音频源 + audioPlayer.value.src = audioSource; + // 设置当前播放状态 currentPlayingId.value = message.id; isPlaying.value = true; @@ -830,8 +844,39 @@ export default { // 播放出错时重置状态 audioPlayer.value.onerror = (e) => { - console.error('音频播放出错:', e); - alert('播放语音失败,可能是格式不支持或数据已损坏'); + const errorDetails = e.target.error ? `错误代码: ${e.target.error.code}` : '未知错误'; + console.error('音频播放出错:', errorDetails); + + // 尝试使用不同的MIME类型重新播放 + if (!message.retryCount) { + message.retryCount = 1; + console.log('尝试使用备用MIME类型重新播放'); + + // 清除当前音频源 + audioPlayer.value.src = ''; + + // 尝试不同的MIME类型 + const alternativeMimeTypes = ['audio/webm', 'audio/ogg', 'audio/mp4', 'audio/wav']; + const currentMimeType = mimeType; + + // 找到一个不同的MIME类型 + let newMimeType = alternativeMimeTypes.find(type => type !== currentMimeType) || 'audio/webm'; + + // 使用新的MIME类型 + message.audio_mime_type = newMimeType; + + // 重新尝试播放 + setTimeout(() => playAudio(message), 100); + return; + } + + // 如果是本地录制的消息,提示可能是暂时性问题 + if (message.isLocal) { + alert('播放本地录制的语音失败,这可能是暂时性问题,语音消息仍会发送给其他用户'); + } else { + alert('播放语音失败,可能是格式不支持或数据已损坏'); + } + currentPlayingId.value = null; isPlaying.value = false; }; @@ -930,7 +975,7 @@ export default { // 1. 获取媒体设备列表 await getMediaDevices() - // 2. 请求麦克风权限并获取音频流 + // 2. 请求麦克风权限并获取音频流(静音状态) const stream = await navigator.mediaDevices.getUserMedia({ audio: { deviceId: selectedAudioInput.value ? { exact: selectedAudioInput.value } : undefined, @@ -941,6 +986,12 @@ export default { video: false }) + // 默认静音开始 + stream.getAudioTracks().forEach(track => { + track.enabled = false + }) + isMuted.value = true + localStream.value = stream // 3. 设置音频分析器(用于显示音量) @@ -953,7 +1004,7 @@ export default { // 5. 标记WebRTC为活跃状态 rtcActive.value = true - addNotification('您已加入语音聊天') + addNotification('您已加入语音聊天(已静音)') isLoadingRTC.value = false } catch (err) { @@ -1345,6 +1396,7 @@ export default { audioPlayer, isRecording, recordingDuration, + rtcActive, localStream, peerConnections, mediaDevices, @@ -1360,7 +1412,7 @@ export default { joinRoom, leaveRoom, sendMessage, - startRecording, + toggleRecording, stopRecording, cancelRecording, playAudio, @@ -1369,7 +1421,7 @@ export default { isCurrentUser, formatTime, toggleVoiceChat, - rtcActive + toggleMute } } } @@ -1836,9 +1888,6 @@ export default { .recording-text { position: absolute; - left: -120px; - top: 50%; - transform: translateY(-50%); white-space: nowrap; font-size: 12px; color: #e74c3c; @@ -1848,6 +1897,9 @@ export default { border-radius: 4px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); z-index: 10; + left: -120px; + top: 50%; + transform: translateY(-50%); } /* 在空间不足时的备选位置 */