Aleksmorshen commited on
Commit
e6a2689
·
verified ·
1 Parent(s): 961d562

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +476 -159
app.py CHANGED
@@ -7,82 +7,177 @@ from flask_socketio import SocketIO, join_room, leave_room, emit
7
  import hashlib
8
 
9
  app = Flask(__name__)
10
- app.config['SECRET_KEY'] = 'your-secret-key-here'
11
  socketio = SocketIO(app)
12
 
13
- # Путь к JSON-файлам
14
- ROOMS_DB = 'rooms.json'
15
- USERS_DB = 'users.json'
16
 
17
- # Загрузка данных из JSON
18
  def load_json(file_path, default={}):
19
- if os.path.exists(file_path):
20
- with open(file_path, 'r') as f:
21
- return json.load(f)
22
- return default
23
-
24
- # Сохранение данных в JSON
 
 
 
 
25
  def save_json(file_path, data):
26
- with open(file_path, 'w') as f:
27
- json.dump(data, f, indent=4)
 
 
 
28
 
29
  # Инициализация баз данных
30
  rooms = load_json(ROOMS_DB)
31
  users = load_json(USERS_DB)
32
 
33
- # Генерация 15-значного токена
34
  def generate_token():
35
  return ''.join(random.choices(string.ascii_letters + string.digits, k=15))
36
 
37
- # Хеширование пароля
38
  def hash_password(password):
39
- return hashlib.sha256(password.encode()).hexdigest()
 
40
 
41
  # Главная страница (регистрация/вход)
42
  @app.route('/', methods=['GET', 'POST'])
43
  def index():
44
  if 'username' in session:
45
  return redirect(url_for('dashboard'))
 
46
  if request.method == 'POST':
47
  action = request.form.get('action')
48
  username = request.form.get('username')
49
  password = request.form.get('password')
50
-
51
  if action == 'register':
52
  if username in users:
53
  return "Пользователь уже существует", 400
 
54
  users[username] = hash_password(password)
55
  save_json(USERS_DB, users)
56
  session['username'] = username
57
  return redirect(url_for('dashboard'))
58
-
59
  elif action == 'login':
60
  if username in users and users[username] == hash_password(password):
61
  session['username'] = username
62
  return redirect(url_for('dashboard'))
63
  return "Неверный логин или пароль", 401
64
-
65
  return render_template_string('''<!DOCTYPE html>
66
- <html>
67
  <head>
 
68
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
69
  <title>Видеоконференция</title>
70
  <style>
71
- body { font-family: Arial, sans-serif; text-align: center; padding: 20px; margin: 0; }
72
- h1 { font-size: 1.5em; }
73
- button, input { padding: 10px; margin: 5px; width: 100%; max-width: 300px; box-sizing: border-box; }
74
- form { display: flex; flex-direction: column; align-items: center; }
75
- @media (max-width: 600px) { h1 { font-size: 1.2em; } button, input { padding: 8px; } }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  </style>
 
77
  </head>
78
  <body>
79
- <h1>Видеоконференция</h1>
80
- <form method="post">
81
- <input type="text" name="username" placeholder="Логин" required>
82
- <input type="password" name="password" placeholder="Пароль" required>
83
- <button type="submit" name="action" value="login">Войти</button>
84
- <button type="submit" name="action" value="register">Зарегистрироваться</button>
85
- </form>
 
 
86
  </body>
87
  </html>''')
88
 
@@ -91,6 +186,7 @@ def index():
91
  def dashboard():
92
  if 'username' not in session:
93
  return redirect(url_for('index'))
 
94
  if request.method == 'POST':
95
  action = request.form.get('action')
96
  if action == 'create':
@@ -103,31 +199,121 @@ def dashboard():
103
  if token in rooms and len(rooms[token]['users']) < rooms[token]['max_users']:
104
  return redirect(url_for('room', token=token))
105
  return "Комната не найдена или переполнена", 404
 
106
  return render_template_string('''<!DOCTYPE html>
107
- <html>
108
  <head>
 
109
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
110
  <title>Панель управления</title>
111
  <style>
112
- body { font-family: Arial, sans-serif; text-align: center; padding: 20px; margin: 0; }
113
- h1 { font-size: 1.5em; }
114
- button, input { padding: 10px; margin: 5px; width: 100%; max-width: 300px; box-sizing: border-box; }
115
- form { display: flex; flex-direction: column; align-items: center; }
116
- @media (max-width: 600px) { h1 { font-size: 1.2em; } button, input { padding: 8px; } }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  </style>
 
118
  </head>
119
  <body>
120
- <h1>Добро пожаловать, {{ session['username'] }}</h1>
121
- <form method="post">
122
- <button type="submit" name="action" value="create">Создать комнату</button>
123
- </form>
124
- <form method="post">
125
- <input type="text" name="token" placeholder="Введите токен комнаты" required>
126
- <button type="submit" name="action" value="join">Войти в комнату</button>
127
- </form>
128
- <form action="/logout" method="post">
129
- <button type="submit">Выйти</button>
130
- </form>
 
 
131
  </body>
132
  </html>''', session=session)
133
 
@@ -144,25 +330,125 @@ def room(token):
144
  return redirect(url_for('index'))
145
  if token not in rooms:
146
  return redirect(url_for('dashboard'))
 
147
  return render_template_string('''<!DOCTYPE html>
148
- <html>
149
  <head>
 
150
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
151
  <title>Комната {{ token }}</title>
152
  <style>
153
- body { font-family: Arial, sans-serif; margin: 0; padding: 10px; }
154
- h1 { font-size: 1.5em; text-align: center; }
155
- #users { margin: 10px 0; text-align: center; font-size: 1em; }
156
- .video-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 10px; padding: 10px; }
157
- video { width: 100%; height: auto; background: black; border-radius: 5px; }
158
- button { padding: 10px; width: 100%; max-width: 300px; margin: 10px auto; display: block; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
  @media (max-width: 600px) {
160
- h1 { font-size: 1.2em; }
161
- .video-grid { grid-template-columns: 1fr; }
162
- video { max-height: 200px; }
163
- button { padding: 8px; }
 
 
164
  }
 
 
 
 
 
 
 
 
165
  </style>
 
 
166
  </head>
167
  <body>
168
  <h1>Комната: {{ token }}</h1>
@@ -170,7 +456,6 @@ def room(token):
170
  <div class="video-grid" id="video-grid"></div>
171
  <button onclick="leaveRoom()">Покинуть комнату</button>
172
 
173
- <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.5.1/socket.io.js"></script>
174
  <script>
175
  const socket = io();
176
  const token = '{{ token }}';
@@ -196,6 +481,9 @@ def room(token):
196
  const existingVideo = document.querySelector(`video[data-user="${user}"]`);
197
  if (existingVideo) return;
198
 
 
 
 
199
  const video = document.createElement('video');
200
  video.srcObject = stream;
201
  video.setAttribute('playsinline', '');
@@ -206,105 +494,127 @@ def room(token):
206
 
207
  if (muted) video.muted = true;
208
  video.dataset.user = user;
209
- document.getElementById('video-grid').appendChild(video);
210
- }
211
 
 
 
 
 
 
212
 
 
 
 
213
 
214
- // Создание соединения с другим пользователем (используется и пр�� инициации, и при ответе)
215
  function createPeerConnection(user) {
216
- if (peers[user]) {
217
- return peers[user].peerConnection; // Return existing connection
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
218
  }
219
-
220
- console.log('Создание RTCPeerConnection для', user);
221
- const peerConnection = new RTCPeerConnection(iceConfig);
222
- peers[user] = { peerConnection: peerConnection, iceCandidates: [] };
223
-
224
- localStream.getTracks().forEach(track => peerConnection.addTrack(track, localStream));
225
-
226
- peerConnection.ontrack = event => {
227
- console.log('Получен поток от', user);
228
- addVideoStream(event.streams[0], user);
229
- };
230
-
231
- peerConnection.onicecandidate = event => {
232
- if (event.candidate) {
233
- console.log('Отправка ICE-кандидата для', user);
234
- socket.emit('signal', {
235
- token: token,
236
- from: username,
237
- to: user,
238
- signal: { type: 'candidate', candidate: event.candidate }
239
- });
240
- }
241
- };
242
- return peerConnection;
243
  }
244
 
245
- // Обработка входящих сигналов
246
  socket.on('signal', data => {
247
- if (data.from === username) return;
248
- console.log('Получен сигнал от', data.from, ':', data.signal.type);
249
-
250
- let peerEntry = peers[data.from];
251
- if (!peerEntry) {
252
- createPeerConnection(data.from);
253
- peerEntry = peers[data.from];
254
- }
255
- let peerConnection = peerEntry.peerConnection;
256
-
257
- if (data.signal.type === 'offer') {
258
- console.log('Обработка предложения от', data.from);
259
- peerConnection.setRemoteDescription(new RTCSessionDescription(data.signal))
260
- .then(() => peerConnection.createAnswer())
261
- .then(answer => peerConnection.setLocalDescription(answer))
262
- .then(() => {
263
- socket.emit('signal', {
264
- token: token,
265
- from: username,
266
- to: data.from,
267
- signal: peerConnection.localDescription
268
- });
269
- //Добавление ICE-кандидатов из буффера
270
- while (peerEntry.iceCandidates.length > 0) {
271
- const candidate = peerEntry.iceCandidates.shift();
272
- peerConnection.addIceCandidate(new RTCIceCandidate(candidate))
273
- .catch(err => console.error("Error adding ice candidate from buffer", err));
274
- }
275
- })
276
- .catch(err => console.error('Ошибка обработки предложения:', err));
277
-
278
- } else if (data.signal.type === 'answer') {
279
- console.log('Обработка ответа от', data.from);
280
- peerConnection.setRemoteDescription(new RTCSessionDescription(data.signal))
281
- .then(() => {
282
- //Добавление ICE-кандидатов из буффера
283
- while (peerEntry.iceCandidates.length > 0) {
284
- const candidate = peerEntry.iceCandidates.shift();
285
- peerConnection.addIceCandidate(new RTCIceCandidate(candidate))
286
- .catch(err => console.error("Error adding ice candidate from buffer", err));
287
- }
288
- })
289
- .catch(err => console.error('Ошибка установки ответа:', err));
290
-
291
- } else if (data.signal.type === 'candidate') {
292
- console.log('Обработка ICE-кандидата от', data.from);
293
- if (peerConnection.remoteDescription) {
294
- peerConnection.addIceCandidate(new RTCIceCandidate(data.signal.candidate))
295
- .catch(err => console.error('Ошибка добавления ICE-кандидата:', err));
296
- } else {
297
- // Если remote description еще не установлен, добавляем в буфер
298
- peerEntry.iceCandidates.push(data.signal.candidate);
299
- console.log("Ice candidate buffered for", data.from)
300
  }
 
 
 
 
 
 
 
 
 
 
 
 
301
  }
 
302
  });
 
303
  socket.on('user_joined', data => {
304
  console.log('Пользователь', data.username, 'присоединился');
305
  document.getElementById('users').innerText = 'Пользователи: ' + data.users.join(', ');
306
 
307
- if (data.username !== username) {
308
  const peerConnection = createPeerConnection(data.username);
309
  // Создание предложения (offer), только если мы инициатор
310
  peerConnection.createOffer()
@@ -321,44 +631,49 @@ def room(token):
321
  }
322
  });
323
 
324
-
325
  socket.on('user_left', data => {
326
- console.log('Пользователь', data.username, 'покинул комнату');
327
- document.getElementById('users').innerText = 'Пользователи: ' + data.users.join(', ');
328
- if (peers[data.username]) {
329
- peers[data.username].peerConnection.close();
330
- delete peers[data.username];
331
- const video = document.querySelector(`video[data-user="${data.username}"]`);
332
- if (video) video.remove();
333
- }
334
  });
335
 
336
  socket.on('init_users', data => {
337
  console.log('Инициализация пользователей:', data.users);
338
  data.users.forEach(user => {
339
- if (user !== username) {
340
  createPeerConnection(user); // Создаем RTCPeerConnection для всех, *кроме себя*
341
  }
342
  });
343
  });
 
344
  function leaveRoom() {
345
  socket.emit('leave', { token: token, username: username });
346
- localStream.getTracks().forEach(track => track.stop());
 
 
347
  for (let user in peers) {
348
  peers[user].peerConnection.close();
349
  }
350
  window.location.href = '/dashboard';
351
  }
 
352
  </script>
353
  </body>
354
  </html>''', token=token, session=session)
355
 
 
 
356
  # WebSocket события
357
  @socketio.on('join')
358
  def handle_join(data):
359
  token = data['token']
360
  username = data['username']
361
-
362
  if token in rooms and len(rooms[token]['users']) < rooms[token]['max_users']:
363
  join_room(token)
364
  if username not in rooms[token]['users']:
@@ -366,12 +681,15 @@ def handle_join(data):
366
  save_json(ROOMS_DB, rooms)
367
  emit('user_joined', {'username': username, 'users': rooms[token]['users']}, room=token)
368
  emit('init_users', {'users': rooms[token]['users']}, to=request.sid)
 
 
 
369
 
370
  @socketio.on('leave')
371
  def handle_leave(data):
372
  token = data['token']
373
  username = data['username']
374
-
375
  if token in rooms and username in rooms[token]['users']:
376
  leave_room(token)
377
  rooms[token]['users'].remove(username)
@@ -380,7 +698,6 @@ def handle_leave(data):
380
 
381
  @socketio.on('signal')
382
  def handle_signal(data):
383
- # Пересылаем сигнал всем в комнате, кроме отправителя
384
  emit('signal', data, room=data['token'], skip_sid=request.sid)
385
 
386
  if __name__ == '__main__':
 
7
  import hashlib
8
 
9
  app = Flask(__name__)
10
+ app.config['SECRET_KEY'] = 'your-very-secret-key-here' # ОЧЕНЬ ВАЖНО: смените на реальный секретный ключ!
11
  socketio = SocketIO(app)
12
 
13
+ # Пути к JSON-файлам (лучше использовать абсолютные пути или пути относительно app.root_path)
14
+ ROOMS_DB = os.path.join(app.root_path, 'rooms.json')
15
+ USERS_DB = os.path.join(app.root_path, 'users.json')
16
 
17
+ # Загрузка данных из JSON (с обработкой ошибок)
18
  def load_json(file_path, default={}):
19
+ try:
20
+ if os.path.exists(file_path):
21
+ with open(file_path, 'r', encoding='utf-8') as f:
22
+ return json.load(f)
23
+ return default
24
+ except (FileNotFoundError, json.JSONDecodeError) as e:
25
+ print(f"Error loading JSON from {file_path}: {e}")
26
+ return default
27
+
28
+ # Сохранение данных в JSON (с обработкой ошибок)
29
  def save_json(file_path, data):
30
+ try:
31
+ with open(file_path, 'w', encoding='utf-8') as f:
32
+ json.dump(data, f, indent=4, ensure_ascii=False)
33
+ except OSError as e:
34
+ print(f"Error saving JSON to {file_path}: {e}")
35
 
36
  # Инициализация баз данных
37
  rooms = load_json(ROOMS_DB)
38
  users = load_json(USERS_DB)
39
 
40
+ # Генерация токена
41
  def generate_token():
42
  return ''.join(random.choices(string.ascii_letters + string.digits, k=15))
43
 
44
+ # Хеширование пароля (более безопасно использовать bcrypt или scrypt)
45
  def hash_password(password):
46
+ return hashlib.sha256(password.encode('utf-8')).hexdigest()
47
+
48
 
49
  # Главная страница (регистрация/вход)
50
  @app.route('/', methods=['GET', 'POST'])
51
  def index():
52
  if 'username' in session:
53
  return redirect(url_for('dashboard'))
54
+
55
  if request.method == 'POST':
56
  action = request.form.get('action')
57
  username = request.form.get('username')
58
  password = request.form.get('password')
59
+
60
  if action == 'register':
61
  if username in users:
62
  return "Пользователь уже существует", 400
63
+ # ВАЖНО: Хешируйте пароли перед сохранением!
64
  users[username] = hash_password(password)
65
  save_json(USERS_DB, users)
66
  session['username'] = username
67
  return redirect(url_for('dashboard'))
68
+
69
  elif action == 'login':
70
  if username in users and users[username] == hash_password(password):
71
  session['username'] = username
72
  return redirect(url_for('dashboard'))
73
  return "Неверный логин или пароль", 401
74
+
75
  return render_template_string('''<!DOCTYPE html>
76
+ <html lang="ru">
77
  <head>
78
+ <meta charset="UTF-8">
79
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
80
  <title>Видеоконференция</title>
81
  <style>
82
+ :root {
83
+ --primary-color: #6200ee; /* Основной цвет */
84
+ --secondary-color: #3700b3; /* Вторичный цвет */
85
+ --background-color: #ffffff; /* Цвет фона */
86
+ --surface-color: #f5f5f5; /* Цвет поверхностей (карточки, формы) */
87
+ --text-color: #333333; /* Основной цвет текста */
88
+ --error-color: #b00020; /* Цвет ошибки */
89
+ --font-family: 'Roboto', sans-serif; /* Шрифт */
90
+
91
+ --border-radius: 12px; /* Радиус скругления */
92
+ --box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1); /* Тень */
93
+ }
94
+
95
+ body {
96
+ font-family: var(--font-family);
97
+ background-color: var(--background-color);
98
+ color: var(--text-color);
99
+ margin: 0;
100
+ padding: 0;
101
+ display: flex;
102
+ justify-content: center;
103
+ align-items: center;
104
+ min-height: 100vh;
105
+ }
106
+
107
+ .container {
108
+ background-color: var(--surface-color);
109
+ padding: 2rem;
110
+ border-radius: var(--border-radius);
111
+ box-shadow: var(--box-shadow);
112
+ width: 90%;
113
+ max-width: 400px;
114
+ text-align: center;
115
+ }
116
+
117
+ h1 {
118
+ font-size: 2rem;
119
+ margin-bottom: 1.5rem;
120
+ color: var(--primary-color);
121
+ }
122
+
123
+ input, button {
124
+ display: block;
125
+ width: 100%;
126
+ padding: 0.75rem;
127
+ margin-bottom: 1rem;
128
+ border: 1px solid #ccc;
129
+ border-radius: var(--border-radius);
130
+ font-size: 1rem;
131
+ box-sizing: border-box;
132
+ transition: border-color 0.3s ease;
133
+ }
134
+
135
+ input:focus {
136
+ outline: none;
137
+ border-color: var(--primary-color);
138
+ }
139
+
140
+ button {
141
+ background-color: var(--primary-color);
142
+ color: white;
143
+ cursor: pointer;
144
+ border: none;
145
+ font-weight: 500;
146
+ transition: background-color 0.3s ease;
147
+ }
148
+
149
+ button:hover {
150
+ background-color: var(--secondary-color);
151
+ }
152
+ button:active {
153
+ opacity: 0.8; /* Немного уменьшаем прозрачность при нажатии */
154
+ }
155
+
156
+ .error-message {
157
+ color: var(--error-color);
158
+ margin-top: 0.5rem;
159
+ }
160
+ @media (prefers-color-scheme: dark) {
161
+ :root {
162
+ --background-color: #121212;
163
+ --surface-color: #1e1e1e;
164
+ --text-color: #ffffff;
165
+ }
166
+ }
167
+
168
  </style>
169
+ <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">
170
  </head>
171
  <body>
172
+ <div class="container">
173
+ <h1>Видеоконференция</h1>
174
+ <form method="post">
175
+ <input type="text" name="username" placeholder="Логин" required>
176
+ <input type="password" name="password" placeholder="Пароль" required>
177
+ <button type="submit" name="action" value="login">Войти</button>
178
+ <button type="submit" name="action" value="register">Зарегистрироваться</button>
179
+ </form>
180
+ </div>
181
  </body>
182
  </html>''')
183
 
 
186
  def dashboard():
187
  if 'username' not in session:
188
  return redirect(url_for('index'))
189
+
190
  if request.method == 'POST':
191
  action = request.form.get('action')
192
  if action == 'create':
 
199
  if token in rooms and len(rooms[token]['users']) < rooms[token]['max_users']:
200
  return redirect(url_for('room', token=token))
201
  return "Комната не найдена или переполнена", 404
202
+
203
  return render_template_string('''<!DOCTYPE html>
204
+ <html lang="ru">
205
  <head>
206
+ <meta charset="UTF-8">
207
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
208
  <title>Панель управления</title>
209
  <style>
210
+ :root {
211
+ --primary-color: #6200ee;
212
+ --secondary-color: #3700b3;
213
+ --background-color: #ffffff;
214
+ --surface-color: #f5f5f5;
215
+ --text-color: #333333;
216
+ --accent-color: #03dac6; /* Цвет акцента (для кнопок, например) */
217
+ --font-family: 'Roboto', sans-serif;
218
+ --border-radius: 12px;
219
+ --box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1);
220
+ }
221
+
222
+ body {
223
+ font-family: var(--font-family);
224
+ background-color: var(--background-color);
225
+ color: var(--text-color);
226
+ margin: 0;
227
+ padding: 0;
228
+ display: flex;
229
+ flex-direction: column;
230
+ align-items: center;
231
+ min-height: 100vh;
232
+ }
233
+
234
+ .container {
235
+ background-color: var(--surface-color);
236
+ padding: 2rem;
237
+ border-radius: var(--border-radius);
238
+ box-shadow: var(--box-shadow);
239
+ width: 90%;
240
+ max-width: 400px;
241
+ text-align: center;
242
+ margin-top: 2rem;
243
+ }
244
+
245
+ h1 {
246
+ font-size: 2rem;
247
+ margin-bottom: 1.5rem;
248
+ color: var(--primary-color);
249
+ }
250
+
251
+ input, button {
252
+ display: block;
253
+ width: 100%;
254
+ padding: 0.75rem;
255
+ margin-bottom: 1rem;
256
+ border: 1px solid #ccc;
257
+ border-radius: var(--border-radius);
258
+ font-size: 1rem;
259
+ box-sizing: border-box;
260
+ transition: border-color 0.3s ease;
261
+ }
262
+
263
+ input:focus {
264
+ outline: none;
265
+ border-color: var(--primary-color);
266
+ }
267
+
268
+ button {
269
+ background-color: var(--primary-color);
270
+ color: white;
271
+ cursor: pointer;
272
+ border: none;
273
+ font-weight: 500;
274
+ transition: background-color 0.3s ease;
275
+ }
276
+ button:hover {
277
+ background-color: var(--secondary-color);
278
+ }
279
+ button:active {
280
+ opacity: 0.8; /* Немного уменьшаем прозрачность при нажатии */
281
+ }
282
+
283
+
284
+ .logout-button {
285
+ background-color: var(--accent-color); /* Другой цвет для кнопки выхода */
286
+ margin-top: 1rem;
287
+ transition: background-color 0.3s ease;
288
+ }
289
+ .logout-button:hover {
290
+ filter: brightness(0.9); /*Затемнение при наведении*/
291
+ }
292
+
293
+ @media (prefers-color-scheme: dark) {
294
+ :root {
295
+ --background-color: #121212;
296
+ --surface-color: #1e1e1e;
297
+ --text-color: #ffffff;
298
+ }
299
+ }
300
  </style>
301
+ <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">
302
  </head>
303
  <body>
304
+ <div class="container">
305
+ <h1>Добро пожаловать, {{ session['username'] }}</h1>
306
+ <form method="post">
307
+ <button type="submit" name="action" value="create">Создать комнату</button>
308
+ </form>
309
+ <form method="post">
310
+ <input type="text" name="token" placeholder="Введите токен комнаты" required>
311
+ <button type="submit" name="action" value="join">Войти в комнату</button>
312
+ </form>
313
+ <form action="/logout" method="post">
314
+ <button class="logout-button" type="submit">Выйти</button>
315
+ </form>
316
+ </div>
317
  </body>
318
  </html>''', session=session)
319
 
 
330
  return redirect(url_for('index'))
331
  if token not in rooms:
332
  return redirect(url_for('dashboard'))
333
+
334
  return render_template_string('''<!DOCTYPE html>
335
+ <html lang="ru">
336
  <head>
337
+ <meta charset="UTF-8">
338
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
339
  <title>Комната {{ token }}</title>
340
  <style>
341
+ :root {
342
+ --primary-color: #6200ee;
343
+ --secondary-color: #3700b3;
344
+ --background-color: #ffffff;
345
+ --surface-color: #f0f0f0; /* Светлый фон для видео */
346
+ --text-color: #333333;
347
+ --font-family: 'Roboto', sans-serif;
348
+ --border-radius: 12px;
349
+ --box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1);
350
+ }
351
+
352
+ body {
353
+ font-family: var(--font-family);
354
+ background-color: var(--background-color);
355
+ color: var(--text-color);
356
+ margin: 0;
357
+ padding: 0;
358
+ }
359
+
360
+ h1 {
361
+ font-size: 1.8rem;
362
+ text-align: center;
363
+ margin: 1rem 0;
364
+ color: var(--primary-color);
365
+ }
366
+
367
+ #users {
368
+ text-align: center;
369
+ margin-bottom: 1rem;
370
+ font-size: 1rem;
371
+ color: var(--secondary-color);
372
+ }
373
+
374
+ .video-grid {
375
+ display: grid;
376
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
377
+ gap: 1rem;
378
+ padding: 1rem;
379
+ background-color: var(--surface-color);
380
+ border-radius: var(--border-radius);
381
+ }
382
+
383
+ video {
384
+ width: 100%;
385
+ height: auto;
386
+ background: black; /* Фон для неотрендеренного видео */
387
+ border-radius: var(--border-radius);
388
+ box-shadow: var(--box-shadow);
389
+ object-fit: cover; /* Важно для правильного отображения */
390
+ display: block; /* Убирает лишние отступы */
391
+ }
392
+ /* Стили для контейнера видео */
393
+ .video-container {
394
+ position: relative; /* Для позиционирования элементов внутри */
395
+ overflow: hidden; /* Скрываем всё, что выходит за границы */
396
+ border-radius: var(--border-radius); /* Скругление углов */
397
+ }
398
+ /* Индикатор пользователя (имя) */
399
+ .user-indicator {
400
+ position: absolute;
401
+ bottom: 0.5rem;
402
+ left: 0.5rem;
403
+ background-color: rgba(0, 0, 0, 0.6); /* Полупрозрачный фон */
404
+ color: white;
405
+ padding: 0.2rem 0.5rem;
406
+ border-radius: 5px;
407
+ font-size: 0.8rem;
408
+ z-index: 10; /* Поверх видео */
409
+ }
410
+
411
+ button {
412
+ display: block;
413
+ width: 80%;
414
+ max-width: 300px;
415
+ padding: 0.75rem;
416
+ margin: 1rem auto;
417
+ background-color: var(--primary-color);
418
+ color: white;
419
+ border: none;
420
+ border-radius: var(--border-radius);
421
+ cursor: pointer;
422
+ font-weight: 500;
423
+ transition: background-color 0.3s ease;
424
+ }
425
+ button:hover {
426
+ background-color: var(--secondary-color);
427
+ }
428
+ button:active {
429
+ opacity: 0.8;
430
+ }
431
+
432
+ /* Адаптивность */
433
  @media (max-width: 600px) {
434
+ .video-grid {
435
+ grid-template-columns: 1fr; /* Одно видео в ряд на мобильных */
436
+ }
437
+ video {
438
+ max-height: 300px; /* Ограничение по высоте на мобильных */
439
+ }
440
  }
441
+ @media (prefers-color-scheme: dark) {
442
+ :root {
443
+ --background-color: #121212;
444
+ --surface-color: #1e1e1e;
445
+ --text-color: #ffffff;
446
+ }
447
+ }
448
+
449
  </style>
450
+ <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">
451
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.5.1/socket.io.js"></script>
452
  </head>
453
  <body>
454
  <h1>Комната: {{ token }}</h1>
 
456
  <div class="video-grid" id="video-grid"></div>
457
  <button onclick="leaveRoom()">Покинуть комнату</button>
458
 
 
459
  <script>
460
  const socket = io();
461
  const token = '{{ token }}';
 
481
  const existingVideo = document.querySelector(`video[data-user="${user}"]`);
482
  if (existingVideo) return;
483
 
484
+ const videoContainer = document.createElement('div'); // Контейнер
485
+ videoContainer.classList.add('video-container');
486
+
487
  const video = document.createElement('video');
488
  video.srcObject = stream;
489
  video.setAttribute('playsinline', '');
 
494
 
495
  if (muted) video.muted = true;
496
  video.dataset.user = user;
 
 
497
 
498
+ // Добавляем индикатор пользователя
499
+ const userIndicator = document.createElement('div');
500
+ userIndicator.classList.add('user-indicator');
501
+ userIndicator.textContent = user;
502
+ videoContainer.appendChild(userIndicator);
503
 
504
+ videoContainer.appendChild(video); // Видео внутрь контейнера
505
+ document.getElementById('video-grid').appendChild(videoContainer); // Добавляем контейнер
506
+ }
507
 
508
+ // Создание соединения
509
  function createPeerConnection(user) {
510
+ if (peers[user]) {
511
+ return peers[user].peerConnection; // Return existing connection
512
+ }
513
+
514
+ console.log('Создание RTCPeerConnection для', user);
515
+ const peerConnection = new RTCPeerConnection(iceConfig);
516
+ peers[user] = { peerConnection: peerConnection, iceCandidates: [] };
517
+
518
+ localStream.getTracks().forEach(track => peerConnection.addTrack(track, localStream));
519
+
520
+ peerConnection.ontrack = event => {
521
+ console.log('Получен поток от', user);
522
+ addVideoStream(event.streams[0], user);
523
+ };
524
+
525
+ // Обработка состояния соединения
526
+ peerConnection.oniceconnectionstatechange = () => {
527
+ console.log(`ICE connection state changed to ${peerConnection.iceConnectionState} for user ${user}`);
528
+ if (peerConnection.iceConnectionState === 'failed' || peerConnection.iceConnectionState === 'disconnected') {
529
+ console.warn(`Connection with ${user} failed or disconnected.`);
530
+ // Можно добавить логику переподключения или удаления пользователя из UI
531
+ if(peers[user]){
532
+ peers[user].peerConnection.close();
533
+ delete peers[user];
534
+ const video = document.querySelector(`video[data-user="${user}"]`);
535
+ if (video) video.parentElement.remove(); // Удалять вместе с контейнером!
536
+ }
537
  }
538
+ };
539
+
540
+ peerConnection.onicecandidate = event => {
541
+ if (event.candidate) {
542
+ console.log('Отправка ICE-кандидата для', user);
543
+ socket.emit('signal', {
544
+ token: token,
545
+ from: username,
546
+ to: user,
547
+ signal: { type: 'candidate', candidate: event.candidate }
548
+ });
549
+ }
550
+ };
551
+ return peerConnection;
 
 
 
 
 
 
 
 
 
 
552
  }
553
 
554
+ // Обработка входящих сигналов
555
  socket.on('signal', data => {
556
+ if (data.from === username) return;
557
+ console.log('Получен сигнал от', data.from, ':', data.signal.type);
558
+
559
+ let peerEntry = peers[data.from];
560
+ if (!peerEntry) {
561
+ createPeerConnection(data.from);
562
+ peerEntry = peers[data.from];
563
+ }
564
+ let peerConnection = peerEntry.peerConnection;
565
+
566
+ if (data.signal.type === 'offer') {
567
+ console.log('Обработка предложения от', data.from);
568
+ peerConnection.setRemoteDescription(new RTCSessionDescription(data.signal))
569
+ .then(() => peerConnection.createAnswer())
570
+ .then(answer => peerConnection.setLocalDescription(answer))
571
+ .then(() => {
572
+ socket.emit('signal', {
573
+ token: token,
574
+ from: username,
575
+ to: data.from,
576
+ signal: peerConnection.localDescription
577
+ });
578
+ //Добавление ICE-кандидатов из буффера
579
+ while (peerEntry.iceCandidates.length > 0) {
580
+ const candidate = peerEntry.iceCandidates.shift();
581
+ peerConnection.addIceCandidate(new RTCIceCandidate(candidate))
582
+ .catch(err => console.error("Error adding ice candidate from buffer", err));
583
+ }
584
+ })
585
+ .catch(err => console.error('Ошибка обработки предложения:', err));
586
+
587
+ } else if (data.signal.type === 'answer') {
588
+ console.log('Обработка ответа от', data.from);
589
+ peerConnection.setRemoteDescription(new RTCSessionDescription(data.signal))
590
+ .then(() => {
591
+ //Добавление ICE-кандидатов из буффера
592
+ while (peerEntry.iceCandidates.length > 0) {
593
+ const candidate = peerEntry.iceCandidates.shift();
594
+ peerConnection.addIceCandidate(new RTCIceCandidate(candidate))
595
+ .catch(err => console.error("Error adding ice candidate from buffer", err));
 
 
 
 
 
 
 
 
 
 
 
 
 
596
  }
597
+ })
598
+ .catch(err => console.error('Ошибка установки ответа:', err));
599
+
600
+ } else if (data.signal.type === 'candidate') {
601
+ console.log('Обработка ICE-кандидата от', data.from);
602
+ if (peerConnection.remoteDescription) {
603
+ peerConnection.addIceCandidate(new RTCIceCandidate(data.signal.candidate))
604
+ .catch(err => console.error('Ошибка добавления ICE-кандидата:', err));
605
+ } else {
606
+ // Если remote description еще не установлен, добавляем в буфер
607
+ peerEntry.iceCandidates.push(data.signal.candidate);
608
+ console.log("Ice candidate buffered for", data.from)
609
  }
610
+ }
611
  });
612
+
613
  socket.on('user_joined', data => {
614
  console.log('Пользователь', data.username, 'присоединился');
615
  document.getElementById('users').innerText = 'Пользователи: ' + data.users.join(', ');
616
 
617
+ if (data.username !== username) {
618
  const peerConnection = createPeerConnection(data.username);
619
  // Создание предложения (offer), только если мы инициатор
620
  peerConnection.createOffer()
 
631
  }
632
  });
633
 
 
634
  socket.on('user_left', data => {
635
+ console.log('Пользователь', data.username, 'покинул комнату');
636
+ document.getElementById('users').innerText = 'Пользователи: ' + data.users.join(', ');
637
+ if (peers[data.username]) {
638
+ peers[data.username].peerConnection.close();
639
+ delete peers[data.username];
640
+ const video = document.querySelector(`video[data-user="${data.username}"]`);
641
+ if (video) video.parentElement.remove(); // Удаляем вместе с контейнером!
642
+ }
643
  });
644
 
645
  socket.on('init_users', data => {
646
  console.log('Инициализация пользователей:', data.users);
647
  data.users.forEach(user => {
648
+ if (user !== username) {
649
  createPeerConnection(user); // Создаем RTCPeerConnection для всех, *кроме себя*
650
  }
651
  });
652
  });
653
+
654
  function leaveRoom() {
655
  socket.emit('leave', { token: token, username: username });
656
+ if (localStream) { // Проверка, что localStream существует
657
+ localStream.getTracks().forEach(track => track.stop());
658
+ }
659
  for (let user in peers) {
660
  peers[user].peerConnection.close();
661
  }
662
  window.location.href = '/dashboard';
663
  }
664
+
665
  </script>
666
  </body>
667
  </html>''', token=token, session=session)
668
 
669
+
670
+
671
  # WebSocket события
672
  @socketio.on('join')
673
  def handle_join(data):
674
  token = data['token']
675
  username = data['username']
676
+
677
  if token in rooms and len(rooms[token]['users']) < rooms[token]['max_users']:
678
  join_room(token)
679
  if username not in rooms[token]['users']:
 
681
  save_json(ROOMS_DB, rooms)
682
  emit('user_joined', {'username': username, 'users': rooms[token]['users']}, room=token)
683
  emit('init_users', {'users': rooms[token]['users']}, to=request.sid)
684
+ else:
685
+ # Отправляем сообщение об ошибке, если комната заполнена
686
+ emit('error_message', {'message': 'Комната переполнена'}, to=request.sid)
687
 
688
  @socketio.on('leave')
689
  def handle_leave(data):
690
  token = data['token']
691
  username = data['username']
692
+
693
  if token in rooms and username in rooms[token]['users']:
694
  leave_room(token)
695
  rooms[token]['users'].remove(username)
 
698
 
699
  @socketio.on('signal')
700
  def handle_signal(data):
 
701
  emit('signal', data, room=data['token'], skip_sid=request.sid)
702
 
703
  if __name__ == '__main__':