发送语音

main
wang 2 months ago
parent 34b78f4aca
commit e8002e1513

@ -20,6 +20,28 @@ app.config['SESSION_COOKIE_SECURE'] = False # 开发环境设为False生产
app.config['SESSION_COOKIE_HTTPONLY'] = True
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
# 确保CORS配置正确处理凭证
CORS(app,
supports_credentials=True,
resources={r"/api/*": {"origins": "*"}},
methods=['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allow_headers=['Content-Type', 'Authorization', 'X-Requested-With'])
# 添加OPTIONS请求的全局处理确保CORS预检请求能够正确响应
@app.after_request
def after_request(response):
# 移除可能已存在的CORS头部
if 'Access-Control-Allow-Origin' in response.headers:
del response.headers['Access-Control-Allow-Origin']
origin = request.headers.get('Origin')
if origin:
response.headers.set('Access-Control-Allow-Origin', origin)
response.headers.set('Access-Control-Allow-Headers', 'Content-Type,Authorization')
response.headers.set('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS')
response.headers.set('Access-Control-Allow-Credentials', 'true')
return response
# 云端MySQL数据库配置
DB_USER = os.environ.get('DB_USER', 'goldminer')
DB_PASSWORD = os.environ.get('DB_PASSWORD', 'nBAWq9DDwJ14Fugq')
@ -34,15 +56,15 @@ app.config['SQLALCHEMY_ECHO'] = True # 开启SQL查询日志方便调试
logger.info(f"连接到云端数据库: {DB_HOST}:{DB_PORT}/{DB_NAME}")
# 确保CORS配置正确处理凭证
CORS(app,
supports_credentials=True,
origins=['*'],
methods=['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allow_headers=['Content-Type', 'Authorization', 'X-Requested-With']) # 允许所有源的凭证请求
db = SQLAlchemy(app)
socketio = SocketIO(app, cors_allowed_origins="*", engineio_logger=True, async_mode='threading')
socketio = SocketIO(app,
cors_allowed_origins="*",
engineio_logger=True,
async_mode='threading',
logger=True,
ping_timeout=60,
ping_interval=25,
always_connect=True)
# 跟踪所有活跃游戏会话
active_games = {} # 用户ID -> 游戏状态
@ -97,6 +119,8 @@ class ChatMessage(db.Model):
room_id = db.Column(db.Integer, db.ForeignKey('chat_room.id'), nullable=False)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
message = db.Column(db.Text, nullable=False)
message_type = db.Column(db.String(20), default='text') # 'text' 或 'audio'
audio_duration = db.Column(db.Float, nullable=True) # 语音消息的持续时间(秒)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
# 建立关系
@ -114,6 +138,26 @@ def init_db():
# 检查是否需要创建默认管理员账户
create_default_admin()
# 检查是否需要添加新的列到ChatMessage表
try:
# 检查列是否已存在并添加
inspector = db.inspect(db.engine)
columns = [column['name'] for column in inspector.get_columns('chat_message')]
# 添加message_type列如果不存在
if 'message_type' not in columns:
db.engine.execute("ALTER TABLE chat_message ADD COLUMN message_type VARCHAR(20) DEFAULT 'text'")
logger.info("已添加message_type列到ChatMessage表")
# 添加audio_duration列如果不存在
if 'audio_duration' not in columns:
db.engine.execute("ALTER TABLE chat_message ADD COLUMN audio_duration FLOAT NULL")
logger.info("已添加audio_duration列到ChatMessage表")
logger.info("已检查并更新ChatMessage表结构。")
except Exception as e:
logger.warning(f"更新ChatMessage表结构时出现问题: {e}")
except OperationalError as e:
logger.error("无法连接到云端MySQL数据库。请检查您的数据库配置和网络连接。")
logger.error(f"错误详情: {e}")
@ -438,13 +482,20 @@ def get_room_messages(room_id):
message_list = []
for msg in messages:
user = User.query.get(msg.user_id)
message_list.append({
message_data = {
'id': msg.id,
'user_id': msg.user_id,
'username': user.username if user else 'Unknown',
'message': msg.message,
'message_type': msg.message_type,
'created_at': msg.created_at.isoformat()
})
}
# 如果是语音消息,添加持续时间
if msg.message_type == 'audio' and msg.audio_duration is not None:
message_data['audio_duration'] = msg.audio_duration
message_list.append(message_data)
return jsonify({'messages': message_list})
@ -570,7 +621,8 @@ def handle_send_message(data):
message = ChatMessage(
room_id=room_id,
user_id=user_id,
message=message_text
message=message_text,
message_type='text'
)
db.session.add(message)
@ -582,6 +634,50 @@ def handle_send_message(data):
'user_id': user_id,
'username': user.username,
'message': message_text,
'message_type': 'text',
'created_at': message.created_at.isoformat()
}, room=str(room_id))
@socketio.on('send_audio_message')
def handle_send_audio_message(data):
user_id = session.get('user_id')
if not user_id:
return
room_id = data.get('room_id')
audio_data = data.get('audio_data') # Base64编码的音频数据
audio_duration = data.get('audio_duration', 0) # 音频持续时间(秒)
if not room_id or not audio_data:
return
# 获取用户和房间信息
user = User.query.get(user_id)
room = ChatRoom.query.get(room_id)
if not user or not room:
return
# 保存消息到数据库
message = ChatMessage(
room_id=room_id,
user_id=user_id,
message=audio_data, # 存储Base64编码的音频数据
message_type='audio',
audio_duration=audio_duration
)
db.session.add(message)
db.session.commit()
# 广播消息给房间内的所有用户
emit('new_message', {
'id': message.id,
'user_id': user_id,
'username': user.username,
'message': audio_data,
'message_type': 'audio',
'audio_duration': audio_duration,
'created_at': message.created_at.isoformat()
}, room=str(room_id))
@ -922,6 +1018,11 @@ def test_admin_status():
}
})
# 处理所有OPTIONS请求
@app.route('/api/<path:path>', methods=['OPTIONS'])
def handle_options(path):
return '', 200
if __name__ == '__main__':
# 在启动应用前初始化数据库
init_db()

@ -0,0 +1,51 @@
import socket
import time
import random
# --- 配置 ---
# 您的后端服务器的IP地址。
# 如果您在另一台电脑上运行此脚本请将其更改为运行后端的电脑的IP地址。
# 如果在同一台电脑上,使用 '127.0.0.1' 即可。
SERVER_IP = '127.0.0.1'
SERVER_PORT = 5001 # 必须与 app.py 中UDP服务器的端口匹配
CLIENT_ID = f'player_{random.randint(1000, 9999)}'
SEND_INTERVAL = 0.5 # 每隔多少秒发送一次数据(模拟游戏帧率)
# ---
def run_udp_client():
"""
模拟一个游戏客户端通过UDP协议向服务器发送高频的游戏状态更新
"""
# 创建一个UDP套接字
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as client_socket:
print(f"UDP客户端 '{CLIENT_ID}' 已启动,将向 {SERVER_IP}:{SERVER_PORT} 发送数据...")
print("按 Ctrl+C 停止。")
hook_angle = 0
direction = 1
try:
while True:
# 模拟钩子来回摆动
if hook_angle > 60:
direction = -1
elif hook_angle < -60:
direction = 1
hook_angle += direction * 5
# 构造消息
message = f"client:{CLIENT_ID};hook_angle:{hook_angle};timestamp:{time.time()}"
# 发送数据
client_socket.sendto(message.encode('utf-8'), (SERVER_IP, SERVER_PORT))
print(f"已发送 -> {message}")
# 等待一段时间再发送下一次更新
time.sleep(SEND_INTERVAL)
except KeyboardInterrupt:
print(f"\n客户端 '{CLIENT_ID}' 已停止。")
if __name__ == "__main__":
run_udp_client()

File diff suppressed because one or more lines are too long

@ -67,9 +67,27 @@
</span>
<span class="timestamp">{{ formatTime(msg.created_at) }}</span>
</div>
<div class="message-content" :class="{ 'current-user-content': isCurrentUser(msg.user_id) }">
<!-- 文本消息 -->
<div v-if="msg.message_type === 'text' || !msg.message_type"
class="message-content"
:class="{ 'current-user-content': isCurrentUser(msg.user_id) }">
{{ msg.message }}
</div>
<!-- 语音消息 -->
<div v-else-if="msg.message_type === 'audio'"
class="message-content audio-message"
:class="{ 'current-user-content': isCurrentUser(msg.user_id) }">
<div class="audio-player">
<button @click="playAudio(msg)" class="play-button">
<i class="audio-icon">🔊</i>
</button>
<div class="audio-waveform">
<div class="duration">{{ formatAudioDuration(msg.audio_duration) }}</div>
</div>
</div>
</div>
</div>
<div v-if="messages.length === 0" class="no-messages">
@ -88,7 +106,20 @@
placeholder="输入消息..."
@keyup.enter="sendMessage"
/>
<button @click="sendMessage" :disabled="!newMessage.trim()">发送</button>
<button @click="sendMessage" :disabled="!newMessage.trim()" class="send-button">发送</button>
<!-- 语音消息按钮 -->
<button
@mousedown="startRecording"
@mouseup="stopRecording"
@mouseleave="cancelRecording"
class="voice-button"
:class="{ 'recording': isRecording }"
title="按住说话"
>
<i class="mic-icon">🎤</i>
<span v-if="isRecording" class="recording-text">... {{ recordingDuration }}s</span>
</button>
</div>
</template>
</div>
@ -113,6 +144,9 @@
</div>
</div>
</div>
<!-- 音频播放元素隐藏 -->
<audio ref="audioPlayer" style="display: none;"></audio>
</div>
</template>
@ -134,6 +168,14 @@ export default {
const newRoomName = ref('')
const showCreateRoomModal = ref(false)
const messagesContainer = ref(null)
const audioPlayer = ref(null)
//
const isRecording = ref(false)
const recordingDuration = ref(0)
const mediaRecorder = ref(null)
const audioChunks = ref([])
const recordingTimer = ref(null)
let socket = null
let currentUser = null
@ -159,14 +201,25 @@ export default {
if (socket) {
socket.disconnect()
}
//
if (recordingTimer.value) {
clearInterval(recordingTimer.value)
}
})
// Socket
const initSocketConnection = () => {
// Socket
socket = io('//', {
withCredentials: true
})
const currentHost = window.location.hostname;
const url = `http://${currentHost}:5000`;
// 使WebSocket
socket = io(url, {
withCredentials: true,
transports: ['polling'],
path: '/socket.io'
});
//
socket.on('connect', () => {
@ -282,7 +335,7 @@ export default {
notifications.value = []
}
//
//
const sendMessage = () => {
if (!currentRoom.value || !newMessage.value.trim()) return
@ -296,6 +349,142 @@ export default {
newMessage.value = ''
}
//
const startRecording = async () => {
try {
//
const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
//
mediaRecorder.value = new MediaRecorder(stream)
audioChunks.value = []
//
mediaRecorder.value.addEventListener('dataavailable', event => {
if (event.data.size > 0) {
audioChunks.value.push(event.data)
}
})
//
mediaRecorder.value.addEventListener('stop', () => {
//
if (audioChunks.value.length === 0 || !isRecording.value) {
isRecording.value = false
recordingDuration.value = 0
stopMediaTracks(stream)
return
}
// Blob
const audioBlob = new Blob(audioChunks.value, { type: 'audio/webm' })
// BlobBase64
const reader = new FileReader()
reader.readAsDataURL(audioBlob)
reader.onloadend = () => {
const base64Audio = reader.result
//
socket.emit('send_audio_message', {
room_id: currentRoom.value.id,
audio_data: base64Audio,
audio_duration: recordingDuration.value
})
//
isRecording.value = false
recordingDuration.value = 0
}
//
stopMediaTracks(stream)
})
//
mediaRecorder.value.start()
isRecording.value = true
//
recordingTimer.value = setInterval(() => {
recordingDuration.value++
// 60
if (recordingDuration.value >= 60) {
stopRecording()
}
}, 1000)
} catch (err) {
console.error('无法访问麦克风:', err)
alert('无法访问麦克风,请检查浏览器权限设置。')
}
}
//
const stopRecording = () => {
if (!mediaRecorder.value || mediaRecorder.value.state === 'inactive') {
return
}
//
mediaRecorder.value.stop()
//
if (recordingTimer.value) {
clearInterval(recordingTimer.value)
recordingTimer.value = null
}
}
//
const cancelRecording = () => {
if (isRecording.value) {
isRecording.value = false
if (mediaRecorder.value && mediaRecorder.value.state === 'recording') {
mediaRecorder.value.stop()
}
//
if (recordingTimer.value) {
clearInterval(recordingTimer.value)
recordingTimer.value = null
}
recordingDuration.value = 0
audioChunks.value = []
}
}
//
const stopMediaTracks = (stream) => {
stream.getTracks().forEach(track => track.stop())
}
//
const playAudio = (message) => {
if (!message || message.message_type !== 'audio') return
if (audioPlayer.value) {
//
audioPlayer.value.pause()
audioPlayer.value.currentTime = 0
//
audioPlayer.value.src = message.message
audioPlayer.value.play()
}
}
//
const formatAudioDuration = (seconds) => {
if (!seconds) return '0:00'
const mins = Math.floor(seconds / 60)
const secs = Math.floor(seconds % 60)
return `${mins}:${secs.toString().padStart(2, '0')}`
}
//
const addNotification = (message) => {
notifications.value.push({
@ -343,11 +532,19 @@ export default {
newRoomName,
showCreateRoomModal,
messagesContainer,
audioPlayer,
isRecording,
recordingDuration,
fetchRooms,
createRoom,
joinRoom,
leaveRoom,
sendMessage,
startRecording,
stopRecording,
cancelRecording,
playAudio,
formatAudioDuration,
isCurrentUser,
formatTime
}
@ -576,6 +773,53 @@ export default {
color: white;
}
/* 语音消息样式 */
.audio-message {
min-width: 120px;
}
.audio-player {
display: flex;
align-items: center;
gap: 10px;
}
.play-button {
background: none;
border: none;
cursor: pointer;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
color: inherit;
}
.audio-icon {
font-size: 20px;
}
.audio-waveform {
flex: 1;
height: 24px;
background: rgba(255, 255, 255, 0.2);
border-radius: 12px;
position: relative;
overflow: hidden;
display: flex;
align-items: center;
justify-content: flex-end;
padding-right: 10px;
}
.current-user-content .audio-waveform {
background: rgba(0, 0, 0, 0.2);
}
.duration {
font-size: 12px;
}
.no-messages {
text-align: center;
padding: 20px;
@ -613,20 +857,74 @@ export default {
border-color: #8B4513;
}
.message-input button {
.send-button {
background-color: #8B4513;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
margin-right: 10px;
}
.message-input button:disabled {
.send-button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
/* 语音录制按钮样式 */
.voice-button {
background-color: #f1f1f1;
border: none;
border-radius: 50%;
width: 44px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
position: relative;
transition: all 0.2s;
}
.voice-button:hover {
background-color: #e0e0e0;
}
.voice-button.recording {
background-color: #e74c3c;
color: white;
box-shadow: 0 0 0 5px rgba(231, 76, 60, 0.3);
animation: pulse 1.5s infinite;
}
.mic-icon {
font-size: 20px;
}
.recording-text {
position: absolute;
bottom: -25px;
left: 50%;
transform: translateX(-50%);
white-space: nowrap;
font-size: 12px;
color: #e74c3c;
font-weight: bold;
}
@keyframes pulse {
0% {
box-shadow: 0 0 0 0 rgba(231, 76, 60, 0.7);
}
70% {
box-shadow: 0 0 0 10px rgba(231, 76, 60, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(231, 76, 60, 0);
}
}
/* 模态框 */
.modal-overlay {
position: fixed;

@ -0,0 +1,7 @@
@echo off
echo 正在停止后端服务...
taskkill /f /im python.exe
echo 正在启动后端服务...
cd backend
python run_server.py
Loading…
Cancel
Save