meeting / app.py
samlax12's picture
Update app.py
5ebc1c7 verified
# -*- coding: utf-8 -*-
from flask import Flask, render_template, request, jsonify, redirect, url_for, session, send_from_directory
from flask_socketio import SocketIO, emit, join_room, leave_room
import os
import random
import string
import time
import logging
import uuid
# 配置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
# 初始化 Flask 应用
app = Flask(__name__, static_folder='static')
app.config['SECRET_KEY'] = os.urandom(24)
socketio = SocketIO(app, cors_allowed_origins="*")
# 存储活跃会议信息
active_meetings = {}
# 存储用户连接信息
connected_users = {}
def generate_meeting_id():
"""生成9位数字的会议ID"""
return ''.join(random.choices(string.digits, k=9))
def format_meeting_id(meeting_id):
"""格式化会议ID为 xxx-xxx-xxx 格式"""
return f"{meeting_id[:3]}-{meeting_id[3:6]}-{meeting_id[6:]}"
# 确保目录结构存在
os.makedirs(os.path.join(os.path.dirname(__file__), 'static'), exist_ok=True)
os.makedirs(os.path.join(os.path.dirname(__file__), 'templates'), exist_ok=True)
# 主页路由
@app.route('/')
def index():
"""主页路由,提供会议登录界面"""
return render_template('index.html')
# 提供静态文件
@app.route('/static/<path:path>')
def serve_static(path):
return send_from_directory('static', path)
# 创建新会议
@app.route('/api/create_meeting', methods=['POST'])
def create_meeting():
"""创建新会议"""
user_name = request.json.get('user_name', '匿名用户')
user_id = str(uuid.uuid4())
meeting_id = generate_meeting_id()
# 创建会议记录
active_meetings[meeting_id] = {
'host': user_id,
'participants': {
user_id: {
'name': user_name,
'role': 'host',
'joined_at': time.time()
}
},
'created_at': time.time(),
'settings': {
'allow_chat': True,
'allow_screen_share': True,
'mute_on_join': True
}
}
# 存储会话信息
session['user_id'] = user_id
session['user_name'] = user_name
session['meeting_id'] = meeting_id
logger.info(f"创建会议: {meeting_id}, 主持人: {user_name} ({user_id})")
formatted_meeting_id = format_meeting_id(meeting_id)
return jsonify({
'success': True,
'meeting_id': formatted_meeting_id,
'raw_meeting_id': meeting_id,
'user_id': user_id,
'is_host': True
})
# 加入会议
@app.route('/api/join_meeting', methods=['POST'])
def join_meeting():
"""加入现有会议"""
user_name = request.json.get('user_name', '匿名用户')
meeting_id = request.json.get('meeting_id', '').replace('-', '')
if not meeting_id or meeting_id not in active_meetings:
return jsonify({
'success': False,
'message': '会议不存在或已结束'
})
user_id = str(uuid.uuid4())
# 添加到参会者列表
active_meetings[meeting_id]['participants'][user_id] = {
'name': user_name,
'role': 'participant',
'joined_at': time.time()
}
# 存储会话信息
session['user_id'] = user_id
session['user_name'] = user_name
session['meeting_id'] = meeting_id
logger.info(f"用户加入会议: {user_name} ({user_id}) -> 会议 {meeting_id}")
formatted_meeting_id = format_meeting_id(meeting_id)
return jsonify({
'success': True,
'meeting_id': formatted_meeting_id,
'raw_meeting_id': meeting_id,
'user_id': user_id,
'is_host': False
})
# 获取会议信息
@app.route('/api/meeting/<meeting_id>')
def meeting_info(meeting_id):
"""获取会议详细信息"""
raw_meeting_id = meeting_id.replace('-', '')
if not raw_meeting_id or raw_meeting_id not in active_meetings:
return jsonify({
'success': False,
'message': '会议不存在或已结束'
})
meeting_data = active_meetings[raw_meeting_id]
participants = [
{
'id': p_id,
'name': p_info['name'],
'role': p_info['role'],
'joined_at': p_info['joined_at']
}
for p_id, p_info in meeting_data['participants'].items()
]
return jsonify({
'success': True,
'meeting_id': meeting_id,
'raw_meeting_id': raw_meeting_id,
'participants': participants,
'host': meeting_data['host'],
'created_at': meeting_data['created_at'],
'settings': meeting_data['settings']
})
# 结束会议
@app.route('/api/end_meeting', methods=['POST'])
def end_meeting():
"""结束会议(仅主持人可操作)"""
user_id = session.get('user_id')
meeting_id = session.get('meeting_id')
if not meeting_id or meeting_id not in active_meetings:
return jsonify({
'success': False,
'message': '会议不存在或已结束'
})
# 验证是否为主持人
if active_meetings[meeting_id]['host'] != user_id:
return jsonify({
'success': False,
'message': '只有主持人可以结束会议'
})
# 移除会议记录
meeting_data = active_meetings.pop(meeting_id)
duration = time.time() - meeting_data['created_at']
logger.info(f"会议结束: {meeting_id}, 持续时间: {duration:.2f}秒")
# 通知所有参会者会议已结束
socketio.emit('meeting_ended', {
'meeting_id': meeting_id,
'message': '主持人已结束会议'
}, room=meeting_id)
return jsonify({
'success': True,
'message': '会议已结束',
'duration': duration
})
# 会议视图路由
@app.route('/meeting/<meeting_id>')
def meeting_room(meeting_id):
"""会议房间页面"""
user_id = session.get('user_id')
user_name = session.get('user_name')
if not user_id or not user_name:
# 用户未登录,重定向到首页
return redirect(url_for('index'))
raw_meeting_id = meeting_id.replace('-', '')
if raw_meeting_id not in active_meetings:
# 会议不存在,重定向到首页
return redirect(url_for('index'))
is_host = (active_meetings[raw_meeting_id]['host'] == user_id)
return render_template(
'meeting.html',
meeting_id=meeting_id,
raw_meeting_id=raw_meeting_id,
user_id=user_id,
user_name=user_name,
is_host=is_host
)
# SocketIO 事件处理
@socketio.on('connect')
def handle_connect():
"""处理用户连接事件"""
sid = request.sid
logger.info(f"新连接: {sid}")
@socketio.on('disconnect')
def handle_disconnect():
"""处理用户断开连接事件"""
sid = request.sid
logger.info(f"连接断开: {sid}")
# 清理用户连接信息
if sid in connected_users:
user_id = connected_users[sid]['user_id']
meeting_id = connected_users[sid]['meeting_id']
if meeting_id in active_meetings and user_id in active_meetings[meeting_id]['participants']:
# 如果是主持人断开连接,不自动移除
if active_meetings[meeting_id]['host'] != user_id:
# 从参会者列表移除
del active_meetings[meeting_id]['participants'][user_id]
# 通知其他参会者
socketio.emit('user_left', {
'user_id': user_id,
'name': connected_users[sid]['name']
}, room=meeting_id)
logger.info(f"用户离开会议: {connected_users[sid]['name']} ({user_id}) 从会议 {meeting_id}")
# 清除连接记录
del connected_users[sid]
@socketio.on('join_room')
def handle_join_room(data):
"""处理用户加入房间事件"""
sid = request.sid
meeting_id = data['meeting_id']
user_id = data['user_id']
user_name = data['user_name']
# 加入房间
join_room(meeting_id)
# 记录连接信息
connected_users[sid] = {
'user_id': user_id,
'meeting_id': meeting_id,
'name': user_name
}
logger.info(f"用户加入房间: {user_name} ({user_id}) -> 会议 {meeting_id}")
# 通知其他参会者有新用户加入
emit('user_joined', {
'user_id': user_id,
'name': user_name,
'is_host': active_meetings[meeting_id]['host'] == user_id if meeting_id in active_meetings else False
}, room=meeting_id, include_self=False)
# 向新用户发送当前参会者列表
if meeting_id in active_meetings:
participants = [
{
'id': p_id,
'name': p_info['name'],
'role': p_info['role'],
'is_host': p_id == active_meetings[meeting_id]['host']
}
for p_id, p_info in active_meetings[meeting_id]['participants'].items()
]
emit('participants_list', {
'participants': participants
})
@socketio.on('leave_room')
def handle_leave_room(data):
"""处理用户离开房间事件"""
sid = request.sid
meeting_id = data['meeting_id']
user_id = data['user_id']
user_name = data['user_name']
# 离开房间
leave_room(meeting_id)
# 从参会者列表移除
if meeting_id in active_meetings and user_id in active_meetings[meeting_id]['participants']:
del active_meetings[meeting_id]['participants'][user_id]
# 通知其他参会者
emit('user_left', {
'user_id': user_id,
'name': user_name
}, room=meeting_id)
logger.info(f"用户离开房间: {user_name} ({user_id}) 从会议 {meeting_id}")
@socketio.on('send_message')
def handle_message(data):
"""处理聊天消息"""
meeting_id = data['meeting_id']
user_id = data['user_id']
user_name = data['user_name']
message = data['message']
timestamp = time.time()
# 检查会议是否存在且聊天功能已启用
if meeting_id not in active_meetings or not active_meetings[meeting_id]['settings']['allow_chat']:
return
# 广播消息给所有参会者
emit('new_message', {
'user_id': user_id,
'user_name': user_name,
'message': message,
'timestamp': timestamp
}, room=meeting_id)
logger.info(f"聊天消息: {user_name} ({user_id}) 在会议 {meeting_id} 中发送消息")
@socketio.on('signal')
def handle_signal(data):
"""处理 WebRTC 信令"""
meeting_id = data['meeting_id']
from_user = data['from']
to_user = data.get('to')
signal_type = data['type']
signal_data = data['data']
if meeting_id not in active_meetings:
return
# 如果指定了接收者,只发送给特定用户
if to_user:
# 查找接收者的 socket id
for sid, user_info in connected_users.items():
if user_info['user_id'] == to_user and user_info['meeting_id'] == meeting_id:
emit('signal', {
'from': from_user,
'type': signal_type,
'data': signal_data
}, room=sid)
break
else:
# 否则广播给房间内的所有人(不包括发送者)
emit('signal', {
'from': from_user,
'type': signal_type,
'data': signal_data
}, room=meeting_id, include_self=False)
@socketio.on('media_status')
def handle_media_status(data):
"""处理媒体状态变更"""
meeting_id = data['meeting_id']
user_id = data['user_id']
media_type = data['media_type'] # 'audio' 或 'video'
enabled = data['enabled']
# 广播状态变更给其他参会者
emit('media_status_change', {
'user_id': user_id,
'media_type': media_type,
'enabled': enabled
}, room=meeting_id, include_self=False)
# 主函数
if __name__ == '__main__':
# 启动服务器
logger.info("视频会议系统服务器启动中...")
socketio.run(app, host='0.0.0.0', port=7860, debug=True, allow_unsafe_werkzeug=True)