Spaces:
Sleeping
Sleeping
from flask import Flask, render_template_string, request, redirect, url_for, session | |
import random | |
import string | |
import json | |
import os | |
from flask_socketio import SocketIO, join_room, leave_room, emit | |
import hashlib | |
app = Flask(__name__) | |
app.config['SECRET_KEY'] = 'your-secret-key-here' | |
socketio = SocketIO(app) | |
# Путь к JSON-файлам | |
ROOMS_DB = 'rooms.json' | |
USERS_DB = 'users.json' | |
# Загрузка данных из JSON | |
def load_json(file_path, default={}): | |
if os.path.exists(file_path): | |
with open(file_path, 'r') as f: | |
return json.load(f) | |
return default | |
# Сохранение данных в JSON | |
def save_json(file_path, data): | |
with open(file_path, 'w') as f: | |
json.dump(data, f, indent=4) | |
# Инициализация баз данных | |
rooms = load_json(ROOMS_DB) | |
users = load_json(USERS_DB) | |
# Генерация 15-значного токена | |
def generate_token(): | |
return ''.join(random.choices(string.ascii_letters + string.digits, k=15)) | |
# Хеширование пароля | |
def hash_password(password): | |
return hashlib.sha256(password.encode()).hexdigest() | |
# Главная страница (регистрация/вход) | |
def index(): | |
if 'username' in session: | |
return redirect(url_for('dashboard')) | |
if request.method == 'POST': | |
action = request.form.get('action') | |
username = request.form.get('username') | |
password = request.form.get('password') | |
if action == 'register': | |
if username in users: | |
return "Пользователь уже существует", 400 | |
users[username] = hash_password(password) | |
save_json(USERS_DB, users) | |
session['username'] = username | |
return redirect(url_for('dashboard')) | |
elif action == 'login': | |
if username in users and users[username] == hash_password(password): | |
session['username'] = username | |
return redirect(url_for('dashboard')) | |
return "Неверный логин или пароль", 401 | |
return render_template_string('''<!DOCTYPE html> | |
<html> | |
<head> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Видеоконференция</title> | |
<style> | |
body { font-family: Arial, sans-serif; text-align: center; padding: 20px; margin: 0; } | |
h1 { font-size: 1.5em; } | |
button, input { padding: 10px; margin: 5px; width: 100%; max-width: 300px; box-sizing: border-box; } | |
form { display: flex; flex-direction: column; align-items: center; } | |
@media (max-width: 600px) { h1 { font-size: 1.2em; } button, input { padding: 8px; } } | |
</style> | |
</head> | |
<body> | |
<h1>Видеоконференция</h1> | |
<form method="post"> | |
<input type="text" name="username" placeholder="Логин" required> | |
<input type="password" name="password" placeholder="Пароль" required> | |
<button type="submit" name="action" value="login">Войти</button> | |
<button type="submit" name="action" value="register">Зарегистрироваться</button> | |
</form> | |
</body> | |
</html>''') | |
# Панель управления | |
def dashboard(): | |
if 'username' not in session: | |
return redirect(url_for('index')) | |
if request.method == 'POST': | |
action = request.form.get('action') | |
if action == 'create': | |
token = generate_token() | |
rooms[token] = {'users': [], 'max_users': 5} | |
save_json(ROOMS_DB, rooms) | |
return redirect(url_for('room', token=token)) | |
elif action == 'join': | |
token = request.form.get('token') | |
if token in rooms and len(rooms[token]['users']) < rooms[token]['max_users']: | |
return redirect(url_for('room', token=token)) | |
return "Комната не найдена или переполнена", 404 | |
return render_template_string('''<!DOCTYPE html> | |
<html> | |
<head> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Панель управления</title> | |
<style> | |
body { font-family: Arial, sans-serif; text-align: center; padding: 20px; margin: 0; } | |
h1 { font-size: 1.5em; } | |
button, input { padding: 10px; margin: 5px; width: 100%; max-width: 300px; box-sizing: border-box; } | |
form { display: flex; flex-direction: column; align-items: center; } | |
@media (max-width: 600px) { h1 { font-size: 1.2em; } button, input { padding: 8px; } } | |
</style> | |
</head> | |
<body> | |
<h1>Добро пожаловать, {{ session['username'] }}</h1> | |
<form method="post"> | |
<button type="submit" name="action" value="create">Создать комнату</button> | |
</form> | |
<form method="post"> | |
<input type="text" name="token" placeholder="Введите токен комнаты" required> | |
<button type="submit" name="action" value="join">Войти в комнату</button> | |
</form> | |
<form action="/logout" method="post"> | |
<button type="submit">Выйти</button> | |
</form> | |
</body> | |
</html>''', session=session) | |
# Выход из системы | |
def logout(): | |
session.pop('username', None) | |
return redirect(url_for('index')) | |
# Страница комнаты | |
def room(token): | |
if 'username' not in session: | |
return redirect(url_for('index')) | |
if token not in rooms: | |
return redirect(url_for('dashboard')) | |
return render_template_string('''<!DOCTYPE html> | |
<html> | |
<head> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Комната {{ token }}</title> | |
<style> | |
body { font-family: Arial, sans-serif; margin: 0; padding: 10px; } | |
h1 { font-size: 1.5em; text-align: center; } | |
#users { margin: 10px 0; text-align: center; font-size: 1em; } | |
.video-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 10px; padding: 10px; } | |
video { width: 100%; height: auto; background: black; border-radius: 5px; } | |
button { padding: 10px; width: 100%; max-width: 300px; margin: 10px auto; display: block; } | |
@media (max-width: 600px) { | |
h1 { font-size: 1.2em; } | |
.video-grid { grid-template-columns: 1fr; } | |
video { max-height: 200px; } | |
button { padding: 8px; } | |
} | |
</style> | |
</head> | |
<body> | |
<h1>Комната: {{ token }}</h1> | |
<div id="users"></div> | |
<div class="video-grid" id="video-grid"></div> | |
<button onclick="leaveRoom()">Покинуть комнату</button> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.5.1/socket.io.js"></script> | |
<script> | |
const socket = io(); | |
const token = '{{ token }}'; | |
const username = '{{ session['username'] }}'; | |
let localStream; | |
const peers = {}; | |
const iceConfig = { | |
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] | |
}; | |
// Получение локального видео/аудио потока | |
navigator.mediaDevices.getUserMedia({ video: true, audio: true }) | |
.then(stream => { | |
localStream = stream; | |
addVideoStream(stream, username, true); | |
socket.emit('join', { token: token, username: username }); | |
}) | |
.catch(err => console.error('Ошибка доступа к камере/микрофону:', err)); | |
// Добавление видео в сетку | |
function addVideoStream(stream, user, muted = false) { | |
console.log('Добавление видео для', user); | |
const existingVideo = document.querySelector(`video[data-user="${user}"]`); | |
if (existingVideo) return; | |
const video = document.createElement('video'); | |
video.srcObject = stream; | |
video.setAttribute('playsinline', ''); | |
video.setAttribute('autoplay', ''); | |
video.addEventListener('loadedmetadata', () => { | |
video.play().catch(e => console.error('Autoplay error:', e)); | |
}); | |
if (muted) video.muted = true; | |
video.dataset.user = user; | |
document.getElementById('video-grid').appendChild(video); | |
} | |
// Создание соединения с другим пользователем (используется и при инициации, и при ответе) | |
function createPeerConnection(user) { | |
if (peers[user]) { | |
return peers[user].peerConnection; // Return existing connection | |
} | |
console.log('Создание RTCPeerConnection для', user); | |
const peerConnection = new RTCPeerConnection(iceConfig); | |
peers[user] = { peerConnection: peerConnection, iceCandidates: [] }; | |
localStream.getTracks().forEach(track => peerConnection.addTrack(track, localStream)); | |
peerConnection.ontrack = event => { | |
console.log('Получен поток от', user); | |
addVideoStream(event.streams[0], user); | |
}; | |
peerConnection.onicecandidate = event => { | |
if (event.candidate) { | |
console.log('Отправка ICE-кандидата для', user); | |
socket.emit('signal', { | |
token: token, | |
from: username, | |
to: user, | |
signal: { type: 'candidate', candidate: event.candidate } | |
}); | |
} | |
}; | |
return peerConnection; | |
} | |
// Обработка входящих сигналов | |
socket.on('signal', data => { | |
if (data.from === username) return; | |
console.log('Получен сигнал от', data.from, ':', data.signal.type); | |
let peerEntry = peers[data.from]; | |
if (!peerEntry) { | |
createPeerConnection(data.from); | |
peerEntry = peers[data.from]; | |
} | |
let peerConnection = peerEntry.peerConnection; | |
if (data.signal.type === 'offer') { | |
console.log('Обработка предложения от', data.from); | |
peerConnection.setRemoteDescription(new RTCSessionDescription(data.signal)) | |
.then(() => peerConnection.createAnswer()) | |
.then(answer => peerConnection.setLocalDescription(answer)) | |
.then(() => { | |
socket.emit('signal', { | |
token: token, | |
from: username, | |
to: data.from, | |
signal: peerConnection.localDescription | |
}); | |
//Добавление ICE-кандидатов из буффера | |
while (peerEntry.iceCandidates.length > 0) { | |
const candidate = peerEntry.iceCandidates.shift(); | |
peerConnection.addIceCandidate(new RTCIceCandidate(candidate)) | |
.catch(err => console.error("Error adding ice candidate from buffer", err)); | |
} | |
}) | |
.catch(err => console.error('Ошибка обработки предложения:', err)); | |
} else if (data.signal.type === 'answer') { | |
console.log('Обработка ответа от', data.from); | |
peerConnection.setRemoteDescription(new RTCSessionDescription(data.signal)) | |
.then(() => { | |
//Добавление ICE-кандидатов из буффера | |
while (peerEntry.iceCandidates.length > 0) { | |
const candidate = peerEntry.iceCandidates.shift(); | |
peerConnection.addIceCandidate(new RTCIceCandidate(candidate)) | |
.catch(err => console.error("Error adding ice candidate from buffer", err)); | |
} | |
}) | |
.catch(err => console.error('Ошибка установки ответа:', err)); | |
} else if (data.signal.type === 'candidate') { | |
console.log('Обработка ICE-кандидата от', data.from); | |
if (peerConnection.remoteDescription) { | |
peerConnection.addIceCandidate(new RTCIceCandidate(data.signal.candidate)) | |
.catch(err => console.error('Ошибка добавления ICE-кандидата:', err)); | |
} else { | |
// Если remote description еще не установлен, добавляем в буфер | |
peerEntry.iceCandidates.push(data.signal.candidate); | |
console.log("Ice candidate buffered for", data.from) | |
} | |
} | |
}); | |
socket.on('user_joined', data => { | |
console.log('Пользователь', data.username, 'присоединился'); | |
document.getElementById('users').innerText = 'Пользователи: ' + data.users.join(', '); | |
if (data.username !== username) { | |
const peerConnection = createPeerConnection(data.username); | |
// Создание предложения (offer), только если мы инициатор | |
peerConnection.createOffer() | |
.then(offer => peerConnection.setLocalDescription(offer)) | |
.then(() => { | |
socket.emit('signal', { | |
token: token, | |
from: username, | |
to: data.username, | |
signal: peerConnection.localDescription | |
}); | |
}) | |
.catch(err => console.error('Ошибка создания предложения:', err)); | |
} | |
}); | |
socket.on('user_left', data => { | |
console.log('Пользователь', data.username, 'покинул комнату'); | |
document.getElementById('users').innerText = 'Пользователи: ' + data.users.join(', '); | |
if (peers[data.username]) { | |
peers[data.username].peerConnection.close(); | |
delete peers[data.username]; | |
const video = document.querySelector(`video[data-user="${data.username}"]`); | |
if (video) video.remove(); | |
} | |
}); | |
socket.on('init_users', data => { | |
console.log('Инициализация пользователей:', data.users); | |
data.users.forEach(user => { | |
if (user !== username) { | |
createPeerConnection(user); // Создаем RTCPeerConnection для всех, *кроме себя* | |
} | |
}); | |
}); | |
function leaveRoom() { | |
socket.emit('leave', { token: token, username: username }); | |
localStream.getTracks().forEach(track => track.stop()); | |
for (let user in peers) { | |
peers[user].peerConnection.close(); | |
} | |
window.location.href = '/dashboard'; | |
} | |
</script> | |
</body> | |
</html>''', token=token, session=session) | |
# WebSocket события | |
def handle_join(data): | |
token = data['token'] | |
username = data['username'] | |
if token in rooms and len(rooms[token]['users']) < rooms[token]['max_users']: | |
join_room(token) | |
if username not in rooms[token]['users']: | |
rooms[token]['users'].append(username) | |
save_json(ROOMS_DB, rooms) | |
emit('user_joined', {'username': username, 'users': rooms[token]['users']}, room=token) | |
emit('init_users', {'users': rooms[token]['users']}, to=request.sid) | |
def handle_leave(data): | |
token = data['token'] | |
username = data['username'] | |
if token in rooms and username in rooms[token]['users']: | |
leave_room(token) | |
rooms[token]['users'].remove(username) | |
save_json(ROOMS_DB, rooms) | |
emit('user_left', {'username': username, 'users': rooms[token]['users']}, room=token) | |
def handle_signal(data): | |
# Пересылаем сигнал всем в комнате, кроме отправителя | |
emit('signal', data, room=data['token'], skip_sid=request.sid) | |
if __name__ == '__main__': | |
socketio.run(app, host='0.0.0.0', port=7860, debug=True, allow_unsafe_werkzeug=True) |