Videospace2 / app.py
Aleksmorshen's picture
Update app.py
da02667 verified
raw
history blame
17 kB
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()
# Главная страница (регистрация/вход)
@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>
<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>''')
# Панель управления
@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}
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)
# Выход из системы
@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'))
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 события
@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)
@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)
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)
if __name__ == '__main__':
socketio.run(app, host='0.0.0.0', port=7860, debug=True, allow_unsafe_werkzeug=True)