Videospace2 / app.py
Aleksmorshen's picture
Update app.py
6249362 verified
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-very-secret-key-here' # ОЧЕНЬ ВАЖНО: смените на реальный секретный ключ!
socketio = SocketIO(app)
# Пути к JSON-файлам (лучше использовать абсолютные пути или пути относительно app.root_path)
ROOMS_DB = os.path.join(app.root_path, 'rooms.json')
USERS_DB = os.path.join(app.root_path, 'users.json')
# Загрузка данных из JSON (с обработкой ошибок)
def load_json(file_path, default={}):
try:
if os.path.exists(file_path):
with open(file_path, 'r', encoding='utf-8') as f:
return json.load(f)
return default
except (FileNotFoundError, json.JSONDecodeError) as e:
print(f"Error loading JSON from {file_path}: {e}")
return default
# Сохранение данных в JSON (с обработкой ошибок)
def save_json(file_path, data):
try:
with open(file_path, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=4, ensure_ascii=False)
except OSError as e:
print(f"Error saving JSON to {file_path}: {e}")
# Инициализация баз данных
rooms = load_json(ROOMS_DB)
users = load_json(USERS_DB)
# Генерация токена
def generate_token():
return ''.join(random.choices(string.ascii_letters + string.digits, k=15))
# Хеширование пароля (более безопасно использовать bcrypt или scrypt)
def hash_password(password):
return hashlib.sha256(password.encode('utf-8')).hexdigest()
# Главная страница (регистрация/вход)
@app.route('/', methods=['GET', 'POST'])
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 lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Видеоконференция</title>
<style>
:root {
--primary-color: #6200ee; /* Основной цвет */
--secondary-color: #3700b3; /* Вторичный цвет */
--background-color: #ffffff; /* Цвет фона */
--surface-color: #f5f5f5; /* Цвет поверхностей (карточки, формы) */
--text-color: #333333; /* Основной цвет текста */
--error-color: #b00020; /* Цвет ошибки */
--font-family: 'Roboto', sans-serif; /* Шрифт */
--border-radius: 12px; /* Радиус скругления */
--box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1); /* Тень */
}
body {
font-family: var(--font-family);
background-color: var(--background-color);
color: var(--text-color);
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
.container {
background-color: var(--surface-color);
padding: 2rem;
border-radius: var(--border-radius);
box-shadow: var(--box-shadow);
width: 90%;
max-width: 400px;
text-align: center;
}
h1 {
font-size: 2rem;
margin-bottom: 1.5rem;
color: var(--primary-color);
}
input, button {
display: block;
width: 100%;
padding: 0.75rem;
margin-bottom: 1rem;
border: 1px solid #ccc;
border-radius: var(--border-radius);
font-size: 1rem;
box-sizing: border-box;
transition: border-color 0.3s ease;
}
input:focus {
outline: none;
border-color: var(--primary-color);
}
button {
background-color: var(--primary-color);
color: white;
cursor: pointer;
border: none;
font-weight: 500;
transition: background-color 0.3s ease;
}
button:hover {
background-color: var(--secondary-color);
}
button:active {
opacity: 0.8; /* Немного уменьшаем прозрачность при нажатии */
}
.error-message {
color: var(--error-color);
margin-top: 0.5rem;
}
@media (prefers-color-scheme: dark) {
:root {
--background-color: #121212;
--surface-color: #1e1e1e;
--text-color: #ffffff;
}
}
</style>
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">
</head>
<body>
<div class="container">
<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>
</div>
</body>
</html>''')
# Панель управления
@app.route('/dashboard', methods=['GET', 'POST'])
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, 'admin': session['username']}
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 lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Панель управления</title>
<style>
:root {
--primary-color: #6200ee;
--secondary-color: #3700b3;
--background-color: #ffffff;
--surface-color: #f5f5f5;
--text-color: #333333;
--accent-color: #03dac6; /* Цвет акцента (для кнопок, например) */
--font-family: 'Roboto', sans-serif;
--border-radius: 12px;
--box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1);
}
body {
font-family: var(--font-family);
background-color: var(--background-color);
color: var(--text-color);
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
}
.container {
background-color: var(--surface-color);
padding: 2rem;
border-radius: var(--border-radius);
box-shadow: var(--box-shadow);
width: 90%;
max-width: 400px;
text-align: center;
margin-top: 2rem;
}
h1 {
font-size: 2rem;
margin-bottom: 1.5rem;
color: var(--primary-color);
}
input, button {
display: block;
width: 100%;
padding: 0.75rem;
margin-bottom: 1rem;
border: 1px solid #ccc;
border-radius: var(--border-radius);
font-size: 1rem;
box-sizing: border-box;
transition: border-color 0.3s ease;
}
input:focus {
outline: none;
border-color: var(--primary-color);
}
button {
background-color: var(--primary-color);
color: white;
cursor: pointer;
border: none;
font-weight: 500;
transition: background-color 0.3s ease;
}
button:hover {
background-color: var(--secondary-color);
}
button:active {
opacity: 0.8; /* Немного уменьшаем прозрачность при нажатии */
}
.logout-button {
background-color: var(--accent-color); /* Другой цвет для кнопки выхода */
margin-top: 1rem;
transition: background-color 0.3s ease;
}
.logout-button:hover {
filter: brightness(0.9); /*Затемнение при наведении*/
}
@media (prefers-color-scheme: dark) {
:root {
--background-color: #121212;
--surface-color: #1e1e1e;
--text-color: #ffffff;
}
}
</style>
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">
</head>
<body>
<div class="container">
<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 class="logout-button" type="submit">Выйти</button>
</form>
</div>
</body>
</html>''', session=session)
# Выход из системы
@app.route('/logout', methods=['POST'])
def logout():
session.pop('username', None)
return redirect(url_for('index'))
# Страница комнаты
@app.route('/room/<token>')
def room(token):
if 'username' not in session:
return redirect(url_for('index'))
is_admin = False # Default to False
if token in rooms:
is_admin = rooms[token].get('admin') == session['username']
else:
return redirect(url_for('dashboard')) # Keep this here
return render_template_string('''<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Комната {{ token }}</title>
<style>
:root {
--primary-color: #6200ee;
--secondary-color: #3700b3;
--background-color: #ffffff;
--surface-color: #f0f0f0; /* Светлый фон для видео */
--text-color: #333333;
--font-family: 'Roboto', sans-serif;
--border-radius: 12px;
--box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1);
--control-bg-color: #eaeaea; /* Цвет фона для элементов управления */
--control-icon-color: #666666; /* Цвет иконок */
}
body {
font-family: var(--font-family);
background-color: var(--background-color);
color: var(--text-color);
margin: 0;
padding: 0;
}
h1 {
font-size: 1.8rem;
text-align: center;
margin: 1rem 0;
color: var(--primary-color);
}
#users {
text-align: center;
margin-bottom: 1rem;
font-size: 1rem;
color: var(--secondary-color);
}
.video-grid-wrapper {
display: flex;
justify-content: center; /* Центрируем сетку по горизонтали */
padding: 1rem;
}
.video-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
max-width: 1200px; /* Ограничиваем максимальную ширину сетки */
width: 100%; /* Растягиваем на всю доступную ширину */
}
video {
width: 100%;
height: auto;
background: black; /* Фон для неотрендеренного видео */
border-radius: var(--border-radius);
box-shadow: var(--box-shadow);
object-fit: cover; /* Важно для правильного отображения */
display: block; /* Убирает лишние отступы */
transform: scaleX(1); /* Убираем отзеркаливание */
}
/* Стили для контейнера видео */
.video-container {
position: relative; /* Для позиционирования элементов внутри */
overflow: hidden; /* Скрываем всё, что выходит за границы */
border-radius: var(--border-radius); /* Скругление углов */
}
/* Индикатор пользователя (имя) */
.user-indicator {
position: absolute;
bottom: 0.5rem;
left: 0.5rem;
background-color: rgba(0, 0, 0, 0.6); /* Полупрозрачный фон */
color: white;
padding: 0.2rem 0.5rem;
border-radius: 5px;
font-size: 0.8rem;
z-index: 10; /* Поверх видео */
}
.controls {
position: absolute;
top: 0.5rem;
right: 0.5rem;
display: flex;
gap: 0.5rem;
z-index: 20;
}
.controls button {
background-color: var(--control-bg-color);
color: var(--control-icon-color);
border: none;
border-radius: 50%; /* Круглые кнопки */
padding: 0.4rem;
cursor: pointer;
font-size: 1rem;
width: 2rem;
height: 2rem;
display: flex; /* Центрируем иконки */
justify-content: center;
align-items: center;
transition: background-color 0.2s, color 0.2s; /* Плавные переходы */
}
.controls button:hover {
background-color: var(--primary-color);
color: white;
}
/* Админские кнопки */
.admin-controls {
position: absolute;
top: 0.5rem;
left: 0.5rem;
display: flex;
gap: 0.3rem;
z-index: 15;
}
.admin-controls button {
background-color: rgba(255, 0, 0, 0.7); /* Полупрозрачный красный */
color: white;
border: none;
border-radius: 5px;
padding: 0.2rem 0.4rem;
font-size: 0.7rem;
cursor: pointer;
transition: background-color 0.2s;
}
.admin-controls button:hover {
background-color: rgba(255, 0, 0, 0.9);
}
.leave-button {
display: block;
width: 80%;
max-width: 300px;
padding: 0.75rem;
margin: 1rem auto;
background-color: var(--primary-color);
color: white;
border: none;
border-radius: var(--border-radius);
cursor: pointer;
font-weight: 500;
transition: background-color 0.3s ease;
}
.leave-button:hover {
background-color: var(--secondary-color);
}
.leave-button:active {
opacity: 0.8;
}
/* Адаптивность */
@media (max-width: 600px) {
.video-grid {
grid-template-columns: 1fr; /* Одно видео в ряд на мобильных */
}
video {
max-height: 300px; /* Ограничение по высоте на мобильных */
}
.controls {
top: auto; /* Переносим кнопки вниз на мобильных */
bottom: 0.5rem;
left: 0.5rem;
right: auto;
}
.admin-controls {
top: auto; /* Переносим админские кнопки */
bottom: 0.5rem;
left: auto;
right: 0.5rem;
}
}
@media (prefers-color-scheme: dark) {
:root {
--background-color: #121212;
--surface-color: #1e1e1e;
--text-color: #ffffff;
--control-bg-color: #333;
--control-icon-color: #eee;
}
}
</style>
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">
<!-- Добавляем Material Icons -->
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.5.1/socket.io.js"></script>
</head>
<body>
<h1>Комната: {{ token }}</h1>
<div id="users"></div>
<div class="video-grid-wrapper">
<div class="video-grid" id="video-grid"></div>
</div>
<button class = "leave-button" onclick="leaveRoom()">Покинуть комнату</button>
<script>
const socket = io();
const token = '{{ token }}';
const username = '{{ session['username'] }}';
const isAdmin = {{ is_admin|tojson }}; // Передаем флаг админа
let localStream;
const peers = {};
const iceConfig = {
iceServers: [{ urls: 'stun:stun.l.google.com:19032' }]
};
// Функция для создания кнопок управления
function createControls(user, videoContainer) {
const controls = document.createElement('div');
controls.classList.add('controls');
const muteAudioButton = document.createElement('button');
muteAudioButton.innerHTML = '<i class="material-icons">mic</i>'; // Иконка микрофона
muteAudioButton.title = "Вкл/Выкл микрофон";
muteAudioButton.dataset.muted = 'false';
muteAudioButton.onclick = () => toggleAudio(user, muteAudioButton);
controls.appendChild(muteAudioButton);
const muteVideoButton = document.createElement('button');
muteVideoButton.innerHTML = '<i class="material-icons">videocam</i>'; // Иконка камеры
muteVideoButton.title = "Вкл/Выкл камеру";
muteVideoButton.dataset.videoMuted = 'false';
muteVideoButton.onclick = () => toggleVideo(user, muteVideoButton);
controls.appendChild(muteVideoButton);
videoContainer.appendChild(controls);
// Админские кнопки (только если текущий пользователь - админ)
if (isAdmin && user !== username) {
createAdminControls(user, videoContainer);
}
}
// Функция для создания админских кнопок
function createAdminControls(targetUser, videoContainer) {
const adminControls = document.createElement('div');
adminControls.classList.add('admin-controls');
const muteUserButton = document.createElement('button');
muteUserButton.innerHTML = 'Mute';
muteUserButton.title = `Mute ${targetUser}`;
muteUserButton.onclick = () => {
socket.emit('admin_mute', { token, targetUser, byUser: username });
};
adminControls.appendChild(muteUserButton);
videoContainer.appendChild(adminControls);
}
// Функции управления звуком/видео (локально)
function toggleAudio(user, button) {
const muted = button.dataset.muted === 'true';
button.dataset.muted = !muted; // Инвертируем состояние
button.innerHTML = `<i class="material-icons">${!muted ? 'mic' : 'mic_off'}</i>`;
if (user === username) {
localStream.getAudioTracks().forEach(track => track.enabled = muted); // Меняем состояние трека
} else if (peers[user]) {
// Для удаленных пользователей, у нас нет прямого доступа к их трекам.
// Вместо этого можно было бы отправить сообщение через сокет, чтобы *попросить* их выключить звук,
// но это уже выходит за рамки простой логики "вкл/выкл".
// console.log("Нельзя напрямую управлять звуком удаленного пользователя.");
}
}
function toggleVideo(user, button) {
const muted = button.dataset.videoMuted === 'true';
button.dataset.videoMuted = !muted;
button.innerHTML = `<i class="material-icons">${!muted ? 'videocam' : 'videocam_off'}</i>`;
if (user === username) {
localStream.getVideoTracks().forEach(track => track.enabled = muted);
} // Для удалённых пользователей аналогично toggleAudio
}
// Получение локального видео/аудио потока
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 videoContainer = document.createElement('div'); // Контейнер
videoContainer.classList.add('video-container');
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;
// Добавляем индикатор пользователя
const userIndicator = document.createElement('div');
userIndicator.classList.add('user-indicator');
userIndicator.textContent = user;
videoContainer.appendChild(userIndicator);
createControls(user, videoContainer); // Создаем кнопки управления
videoContainer.appendChild(video); // Видео внутрь контейнера
document.getElementById('video-grid').appendChild(videoContainer); // Добавляем контейнер
}
// Создание соединения
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.oniceconnectionstatechange = () => {
console.log(`ICE connection state changed to ${peerConnection.iceConnectionState} for user ${user}`);
if (peerConnection.iceConnectionState === 'failed' || peerConnection.iceConnectionState === 'disconnected') {
console.warn(`Connection with ${user} failed or disconnected.`);
// Можно добавить логику переподключения или удаления пользователя из UI
if(peers[user]){
peers[user].peerConnection.close();
delete peers[user];
const video = document.querySelector(`video[data-user="${user}"]`);
if (video) video.parentElement.remove(); // Удалять вместе с контейнером!
}
}
};
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.parentElement.remove(); // Удаляем вместе с контейнером!
}
});
socket.on('init_users', data => {
console.log('Инициализация пользователей:', data.users);
data.users.forEach(user => {
if (user !== username) {
createPeerConnection(user); // Создаем RTCPeerConnection для всех, *кроме себя*
}
});
});
socket.on('admin_muted', data => { // Обработка события мута от админа
if (data.targetUser === username) {
// Выключаем микрофон (локально)
localStream.getAudioTracks().forEach(track => track.enabled = false);
// Обновляем состояние кнопки (если есть)
const muteButton = document.querySelector(`.controls button[data-muted][data-user="${username}"]`);
if (muteButton) {
muteButton.dataset.muted = 'true';
muteButton.innerHTML = '<i class="material-icons">mic_off</i>';
}
}
});
function leaveRoom() {
socket.emit('leave', { token: token, username: username });
if (localStream) { // Проверка, что localStream существует
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, is_admin=is_admin)
# WebSocket события
@socketio.on('join')
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)
else:
# Отправляем сообщение об ошибке, если комната заполнена
emit('error_message', {'message': 'Комната переполнена'}, to=request.sid)
@socketio.on('leave')
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)
# Если админ выходит, комнату нужно пометить, или назначить нового админа. Простейший вариант - удалить.
if rooms[token]['admin'] == username:
del rooms[token]
save_json(ROOMS_DB, rooms)
emit('user_left', {'username': username, 'users': rooms[token]['users']}, room=token)
@socketio.on('signal')
def handle_signal(data):
emit('signal', data, room=data['token'], skip_sid=request.sid)
@socketio.on('admin_mute')
def handle_admin_mute(data):
token = data['token']
target_user = data['targetUser']
by_user = data['byUser'] # Кто замьютил
# Проверяем, является ли отправитель админом комнаты
if token in rooms and rooms[token].get('admin') == by_user:
emit('admin_muted', {'targetUser': target_user}, room=token) # Отправляем событие всем в комнате
if __name__ == '__main__':
socketio.run(app, host='0.0.0.0', port=7860, debug=True, allow_unsafe_werkzeug=True)