|
|
|
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__) |
|
|
|
|
|
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.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: |
|
|
|
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'] |
|
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) |