# -*- 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/') 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/') 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/') 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)