Aleksmorshen commited on
Commit
eb179ca
·
verified ·
1 Parent(s): 186a8b9

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +275 -221
app.py CHANGED
@@ -14,7 +14,7 @@ socketio = SocketIO(app)
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):
@@ -25,7 +25,6 @@ def load_json(file_path, default={}):
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:
@@ -33,20 +32,16 @@ def save_json(file_path, data):
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
- # Хеширование пароля
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:
@@ -79,7 +74,7 @@ def index():
79
  <title>Видеоконференция</title>
80
  <style>
81
  /* ... (стили из предыдущих ответов, без изменений) ... */
82
- :root {
83
  --primary-color: #6200ee; /* Основной цвет */
84
  --secondary-color: #3700b3; /* Вторичный цвет */
85
  --background-color: #ffffff; /* Цвет фона */
@@ -207,7 +202,7 @@ def dashboard():
207
  <title>Панель управления</title>
208
  <style>
209
  /* ... (стили из предыдущих ответов, без изменений) ... */
210
- :root {
211
  --primary-color: #6200ee;
212
  --secondary-color: #3700b3;
213
  --background-color: #ffffff;
@@ -335,13 +330,15 @@ def room(token):
335
  video_url = rooms[token]['video_url']
336
  video_state = rooms[token]['video_state']
337
 
338
- return render_template_string('''<!DOCTYPE html>
 
339
  <html lang="ru">
340
  <head>
341
  <meta charset="UTF-8">
342
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
343
  <title>Комната {{ token }}</title>
344
  <style>
 
345
  :root {
346
  --primary-color: #4CAF50; /* Зеленый */
347
  --secondary-color: #388E3C; /* Темно-зеленый */
@@ -660,27 +657,26 @@ def room(token):
660
 
661
  <div class="main-container">
662
  <div class="video-section">
663
- <div id="shared-video-container">
664
- {% if video_url %}
665
- <iframe id="shared-video" src="{{ video_url }}" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
666
- <div id="video-controls">
667
- <button id="play-pause" data-state="paused"><i class="material-icons">play_arrow</i></button>
668
- <button id="seek-backward"><i class="material-icons">replay_10</i></button>
669
- <button id="seek-forward"><i class="material-icons">forward_10</i></button>
670
- </div>
671
- {% endif %}
672
- </div>
673
- {% if is_admin %}
674
- <div class="admin-video-controls">
675
  <input type="text" id="video-url-input" placeholder="URL видео (YouTube, Rutube, VK)">
676
  <button id="set-video-url">Установить</button>
677
- </div>
678
- {% endif %}
679
-
680
-
681
  <button class="leave-button" onclick="leaveRoom()">Покинуть комнату</button>
682
  </div>
683
- <div class="video-grid-wrapper">
 
684
  <div class="video-grid" id="video-grid"></div>
685
  </div>
686
  </div>
@@ -695,12 +691,10 @@ def room(token):
695
  const iceConfig = {
696
  iceServers: [{ urls: 'stun:stun.l.google.com:19032' }]
697
  };
698
-
699
  let videoState = {{ video_state|tojson }};
700
 
701
-
702
- // Функции для работы с общим видео
703
- function setupSharedVideo() {
704
  const video = document.getElementById('shared-video');
705
  if (!video) return;
706
 
@@ -777,11 +771,9 @@ def room(token):
777
  }
778
  }
779
 
780
-
781
  socket.on('video_url_updated', (data) => {
782
-
783
- const videoContainer = $('#shared-video-container'); // Используем jQuery
784
- // Очищаем контейнер и создаем новый iframe
785
  videoContainer.empty();
786
 
787
  const newIframe = $('<iframe>', {
@@ -791,42 +783,32 @@ def room(token):
791
  allow: 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share',
792
  allowfullscreen: true
793
  });
794
-
795
- // Добавляем iframe в контейнер
796
  videoContainer.append(newIframe);
797
 
798
- // Добавляем кнопки управления
799
- const videoControls = $('<div>', { id: 'video-controls' });
800
  videoControls.append($('<button>', { id: 'play-pause', 'data-state': 'paused' }).html('<i class="material-icons">play_arrow</i>'));
801
  videoControls.append($('<button>', { id: 'seek-backward' }).html('<i class="material-icons">replay_10</i>'));
802
  videoControls.append($('<button>', { id: 'seek-forward' }).html('<i class="material-icons">forward_10</i>'));
803
  videoContainer.append(videoControls);
804
 
805
  videoState = { playing: false, time: 0 };
806
- setupSharedVideo();
807
  });
808
 
809
  socket.on('video_controlled', (data) => {
810
- videoState = data.state;
811
- if (data.action === 'play_pause') {
812
- playPauseVideo(data.state.playing);
813
- } else if (data.action === 'seek') {
814
- seekVideo(data.state.time);
815
- }
816
- const playPauseButton = document.getElementById('play-pause');
817
  if (playPauseButton) {
818
  playPauseButton.innerHTML = `<i class="material-icons">${data.state.playing ? 'pause' : 'play_arrow'}</i>`;
819
  }
820
  });
821
 
822
- $(document).ready(() => {
823
- setupSharedVideo();
824
- });
825
- socket.on('initial_video_state', (data) => {
826
- videoState = data.state;
827
- setupSharedVideo();
828
- });
829
-
830
 
831
  function createControls(user, videoContainer) {
832
  const controls = document.createElement('div');
@@ -860,222 +842,233 @@ def room(token):
860
  const muteUserButton = document.createElement('button');
861
  muteUserButton.innerHTML = 'Mute';
862
  muteUserButton.title = `Mute ${targetUser}`;
863
- muteUserButton. muteUserButton.onclick = () => {
864
  socket.emit('admin_mute', { token, targetUser, byUser: username });
865
  };
866
- adminControls.appendChild(muteUserButton);
867
  videoContainer.appendChild(adminControls);
868
  }
869
 
870
- // Функции управления звуком/видео (локально)
871
  function toggleAudio(user, button) {
872
- const muted = button.dataset.muted === 'true';
873
- button.dataset.muted = !muted; // Инвертируем состояние
874
- button.innerHTML = `<i class="material-icons">${!muted ? 'mic' : 'mic_off'}</i>`;
875
-
876
- if (user === username) {
877
- localStream.getAudioTracks().forEach(track => track.enabled = muted); // Меняем состояние трека
878
- }
879
  }
880
 
881
  function toggleVideo(user, button) {
882
- const muted = button.dataset.videoMuted === 'true';
883
- button.dataset.videoMuted = !muted;
884
- button.innerHTML = `<i class="material-icons">${!muted ? 'videocam' : 'videocam_off'}</i>`;
885
-
886
  if (user === username) {
887
- localStream.getVideoTracks().forEach(track => track.enabled = muted);
888
  }
889
  }
890
 
891
 
892
- // Получение локального видео/аудио потока
893
- navigator.mediaDevices.getUserMedia({ video: true, audio: true })
894
- .then(stream => {
895
- localStream = stream;
896
- addVideoStream(stream, username, true);
897
- socket.emit('join', { token: token, username: username });
898
- })
899
- .catch(err => console.error('Ошибка доступа к камере/микрофону:', err));
900
-
901
- // Добавление видео в сетку
902
- function addVideoStream(stream, user, muted = false) {
903
- console.log('Добавление видео для', user);
904
  const existingVideo = document.querySelector(`video[data-user="${user}"]`);
905
- if (existingVideo) return;
 
 
 
 
906
 
907
- const videoContainer = document.createElement('div'); // Контейнер
908
  videoContainer.classList.add('video-container');
909
 
910
  const video = document.createElement('video');
911
  video.srcObject = stream;
912
- video.setAttribute('playsinline', '');
913
- video.setAttribute('autoplay', '');
 
 
 
 
 
 
914
  video.addEventListener('loadedmetadata', () => {
915
- video.play().catch(e => console.error('Autoplay error:', e));
916
  });
917
 
918
- if (muted) video.muted = true;
919
- video.dataset.user = user;
920
 
921
- // Добавляем индикатор пользователя
922
  const userIndicator = document.createElement('div');
923
  userIndicator.classList.add('user-indicator');
924
  userIndicator.textContent = user;
925
  videoContainer.appendChild(userIndicator);
926
- createControls(user, videoContainer); // Создаем кнопки управления
927
 
928
- videoContainer.appendChild(video); // Видео внутрь контейнера
929
- document.getElementById('video-grid').appendChild(videoContainer); // Добавляем контейнер
 
 
930
  }
931
 
932
- // Создание соединения
933
- function createPeerConnection(user) {
934
- if (peers[user]) {
935
- return peers[user].peerConnection; // Return existing connection
936
- }
937
 
938
- console.log('Создание RTCPeerConnection для', user);
939
- const peerConnection = new RTCPeerConnection(iceConfig);
940
- peers[user] = { peerConnection: peerConnection, iceCandidates: [] };
941
-
942
- localStream.getTracks().forEach(track => peerConnection.addTrack(track, localStream));
943
-
944
- peerConnection.ontrack = event => {
945
- console.log('Получен поток от', user);
946
- addVideoStream(event.streams[0], user);
947
- };
948
-
949
- // Обработка состояния соединения
950
- peerConnection.oniceconnectionstatechange = () => {
951
- console.log(`ICE connection state changed to ${peerConnection.iceConnectionState} for user ${user}`);
952
- if (peerConnection.iceConnectionState === 'failed' || peerConnection.iceConnectionState === 'disconnected') {
953
- console.warn(`Connection with ${user} failed or disconnected.`);
954
- if(peers[user]){
955
- peers[user].peerConnection.close();
956
- delete peers[user];
957
- const video = document.querySelector(`video[data-user="${user}"]`);
958
- if (video) video.parentElement.remove();
959
- }
960
- }
961
- };
962
-
963
- peerConnection.onicecandidate = event => {
964
- if (event.candidate) {
965
- console.log('Отправка ICE-кандидата для', user);
966
- socket.emit('signal', {
967
- token: token,
968
- from: username,
969
- to: user,
970
- signal: { type: 'candidate', candidate: event.candidate }
971
- });
972
  }
973
- };
974
- return peerConnection;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
975
  }
976
 
977
- // Обработка входящих сигналов
978
  socket.on('signal', data => {
979
- if (data.from === username) return;
980
- console.log('Получен сигнал от', data.from, ':', data.signal.type);
981
 
982
- let peerEntry = peers[data.from];
983
- if (!peerEntry) {
984
- createPeerConnection(data.from);
985
- peerEntry = peers[data.from];
986
- }
987
- let peerConnection = peerEntry.peerConnection;
988
-
989
- if (data.signal.type === 'offer') {
990
- console.log('Обработка предложения от', data.from);
991
- peerConnection.setRemoteDescription(new RTCSessionDescription(data.signal))
992
- .then(() => peerConnection.createAnswer())
993
- .then(answer => peerConnection.setLocalDescription(answer))
994
- .then(() => {
995
- socket.emit('signal', {
996
- token: token,
997
- from: username,
998
- to: data.from,
999
- signal: peerConnection.localDescription
1000
- });
1001
- //Добавление ICE-кандидатов из буффера
1002
- while (peerEntry.iceCandidates.length > 0) {
1003
- const candidate = peerEntry.iceCandidates.shift();
1004
- peerConnection.addIceCandidate(new RTCIceCandidate(candidate))
1005
- .catch(err => console.error("Error adding ice candidate from buffer", err));
1006
- }
1007
- })
1008
- .catch(err => console.error('Ошибка обработки предложения:', err));
1009
-
1010
- } else if (data.signal.type === 'answer') {
1011
- console.log('Обработка ответа от', data.from);
1012
- peerConnection.setRemoteDescription(new RTCSessionDescription(data.signal))
1013
- .then(() => {
1014
- //Добавление ICE-кандидатов из буффера
1015
- while (peerEntry.iceCandidates.length > 0) {
1016
- const candidate = peerEntry.iceCandidates.shift();
1017
- peerConnection.addIceCandidate(new RTCIceCandidate(candidate))
1018
- .catch(err => console.error("Error adding ice candidate from buffer", err));
 
 
 
 
 
 
 
 
 
 
1019
  }
1020
- })
1021
- .catch(err => console.error('Ошибка установки ответа:', err));
1022
-
1023
- } else if (data.signal.type === 'candidate') {
1024
- console.log('Обработка ICE-кандидата от', data.from);
1025
- if (peerConnection.remoteDescription) {
1026
- peerConnection.addIceCandidate(new RTCIceCandidate(data.signal.candidate))
1027
- .catch(err => console.error('Ошибка добавления ICE-кандидата:', err));
1028
- } else {
1029
- // Если remote description еще не установлен, добавляем в буфер
1030
- peerEntry.iceCandidates.push(data.signal.candidate);
1031
- console.log("Ice candidate buffered for", data.from)
1032
  }
1033
- }
1034
  });
1035
 
1036
  socket.on('user_joined', data => {
1037
- console.log('Пользователь', data.username, 'присоединился');
1038
- document.getElementById('users').innerText = 'Пользователи: ' + data.users.join(', ');
1039
 
1040
- if (data.username !== username) {
 
1041
  const peerConnection = createPeerConnection(data.username);
 
1042
  peerConnection.createOffer()
1043
  .then(offer => peerConnection.setLocalDescription(offer))
1044
  .then(() => {
1045
  socket.emit('signal', {
1046
- token: token,
1047
- from: username,
1048
  to: data.username,
 
 
1049
  signal: peerConnection.localDescription
1050
  });
1051
  })
1052
- .catch(err => console.error('Ошибка создания предложения:', err));
1053
  }
1054
  });
1055
 
1056
- socket.on('user_left', data => {
1057
- console.log('Пользователь', data.username, 'покинул комнату');
1058
- document.getElementById('users').innerText = 'Пользователи: ' + data.users.join(', ');
1059
- if (peers[data.username]) {
1060
- peers[data.username].peerConnection.close();
1061
- delete peers[data.username];
1062
- const video = document.querySelector(`video[data-user="${data.username}"]`);
1063
- if (video) video.parentElement.remove();
1064
- }
1065
- });
1066
 
1067
  socket.on('init_users', data => {
1068
- console.log('Инициализация пользователей:', data.users);
1069
- data.users.forEach(user => {
1070
- if (user !== username) {
1071
- createPeerConnection(user);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1072
  }
1073
- });
1074
  });
1075
- socket.on('admin_muted', data => {
 
1076
  if (data.targetUser === username) {
1077
  localStream.getAudioTracks().forEach(track => track.enabled = false);
1078
-
1079
  const muteButton = document.querySelector(`.controls button[data-muted][data-user="${username}"]`);
1080
  if (muteButton) {
1081
  muteButton.dataset.muted = 'true';
@@ -1085,9 +1078,9 @@ def room(token):
1085
  });
1086
 
1087
  function leaveRoom() {
1088
- socket.emit('leave', { token: token, username: username });
1089
  if (localStream) {
1090
- localStream.getTracks().forEach(track => track.stop());
1091
  }
1092
  for (let user in peers) {
1093
  peers[user].peerConnection.close();
@@ -1095,47 +1088,56 @@ def room(token):
1095
  window.location.href = '/dashboard';
1096
  }
1097
 
 
 
 
1098
  </script>
1099
  </body>
1100
  </html>
1101
  ''', token=token, session=session, is_admin=is_admin, video_url=video_url, video_state=video_state)
1102
 
1103
 
1104
-
1105
- # WebSocket события
1106
  @socketio.on('join')
1107
  def handle_join(data):
1108
  token = data['token']
1109
  username = data['username']
 
1110
 
1111
  if token in rooms and len(rooms[token]['users']) < rooms[token]['max_users']:
1112
  join_room(token)
1113
  if username not in rooms[token]['users']:
1114
- rooms[token]['users'].append(username)
1115
- save_json(ROOMS_DB, rooms)
 
1116
  emit('initial_video_state', {'state': rooms[token]['video_state']}, to=request.sid)
 
1117
  emit('user_joined', {'username': username, 'users': rooms[token]['users']}, room=token)
 
1118
  emit('init_users', {'users': rooms[token]['users']}, to=request.sid)
1119
-
1120
  else:
1121
- emit('error_message', {'message': 'Комната переполнена'}, to=request.sid)
1122
 
1123
  @socketio.on('leave')
1124
  def handle_leave(data):
1125
  token = data['token']
1126
  username = data['username']
 
1127
 
1128
  if token in rooms and username in rooms[token]['users']:
1129
  leave_room(token)
1130
  rooms[token]['users'].remove(username)
1131
  if rooms[token]['admin'] == username:
1132
- del rooms[token]
1133
  save_json(ROOMS_DB, rooms)
1134
  emit('user_left', {'username': username, 'users': rooms[token]['users']}, room=token)
1135
 
 
1136
  @socketio.on('signal')
1137
  def handle_signal(data):
1138
- emit('signal', data, room=data['token'], skip_sid=request.sid)
 
 
1139
 
1140
  @socketio.on('admin_mute')
1141
  def handle_admin_mute(data):
@@ -1150,8 +1152,20 @@ def handle_admin_mute(data):
1150
  def handle_set_video_url(data):
1151
  token = data['token']
1152
  video_url = data['video_url']
 
1153
 
1154
  if token in rooms and rooms[token]['admin'] == session.get('username'):
 
 
 
 
 
 
 
 
 
 
 
1155
  rooms[token]['video_url'] = video_url
1156
  rooms[token]['video_state'] = {'playing': False, 'time': 0}
1157
  save_json(ROOMS_DB, rooms)
@@ -1162,7 +1176,7 @@ def handle_video_control(data):
1162
  token = data['token']
1163
  action = data['action']
1164
  if token not in rooms:
1165
- return
1166
 
1167
  if action == 'play_pause':
1168
  rooms[token]['video_state']['playing'] = data['state']['playing']
@@ -1172,5 +1186,45 @@ def handle_video_control(data):
1172
  save_json(ROOMS_DB, rooms)
1173
  emit('video_controlled', {'action': action, 'state': rooms[token]['video_state']}, room=token)
1174
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1175
  if __name__ == '__main__':
1176
  socketio.run(app, host='0.0.0.0', port=7860, debug=True, allow_unsafe_werkzeug=True)
 
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):
 
25
  print(f"Error loading JSON from {file_path}: {e}")
26
  return default
27
 
 
28
  def save_json(file_path, data):
29
  try:
30
  with open(file_path, 'w', encoding='utf-8') as f:
 
32
  except OSError as e:
33
  print(f"Error saving JSON to {file_path}: {e}")
34
 
 
35
  rooms = load_json(ROOMS_DB)
36
  users = load_json(USERS_DB)
37
 
 
38
  def generate_token():
39
  return ''.join(random.choices(string.ascii_letters + string.digits, k=15))
40
 
 
41
  def hash_password(password):
42
  return hashlib.sha256(password.encode('utf-8')).hexdigest()
43
 
44
+ # ... (остальные функции: index, dashboard, logout - без изменений) ...
 
45
  @app.route('/', methods=['GET', 'POST'])
46
  def index():
47
  if 'username' in session:
 
74
  <title>Видеоконференция</title>
75
  <style>
76
  /* ... (стили из предыдущих ответов, без изменений) ... */
77
+ :root {
78
  --primary-color: #6200ee; /* Основной цвет */
79
  --secondary-color: #3700b3; /* Вторичный цвет */
80
  --background-color: #ffffff; /* Цвет фона */
 
202
  <title>Панель управления</title>
203
  <style>
204
  /* ... (стили из предыдущих ответов, без изменений) ... */
205
+ :root {
206
  --primary-color: #6200ee;
207
  --secondary-color: #3700b3;
208
  --background-color: #ffffff;
 
330
  video_url = rooms[token]['video_url']
331
  video_state = rooms[token]['video_state']
332
 
333
+ return render_template_string('''
334
+ <!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
+ /* ... (стили из последнего полного листинга, с небольшими изменениями) ... */
342
  :root {
343
  --primary-color: #4CAF50; /* Зеленый */
344
  --secondary-color: #388E3C; /* Темно-зеленый */
 
657
 
658
  <div class="main-container">
659
  <div class="video-section">
660
+ <div id="shared-video-container">
661
+ {% if video_url %}
662
+ <iframe id="shared-video" src="{{ video_url }}" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
663
+ <div id="video-controls">
664
+ <button id="play-pause" data-state="paused"><i class="material-icons">play_arrow</i></button>
665
+ <button id="seek-backward"><i class="material-icons">replay_10</i></button>
666
+ <button id="seek-forward"><i class="material-icons">forward_10</i></button>
667
+ </div>
668
+ {% endif %}
669
+ </div>
670
+ {% if is_admin %}
671
+ <div class="admin-video-controls">
672
  <input type="text" id="video-url-input" placeholder="URL видео (YouTube, Rutube, VK)">
673
  <button id="set-video-url">Установить</button>
674
+ </div>
675
+ {% endif %}
 
 
676
  <button class="leave-button" onclick="leaveRoom()">Покинуть комнату</button>
677
  </div>
678
+
679
+ <div class="video-grid-wrapper">
680
  <div class="video-grid" id="video-grid"></div>
681
  </div>
682
  </div>
 
691
  const iceConfig = {
692
  iceServers: [{ urls: 'stun:stun.l.google.com:19032' }]
693
  };
 
694
  let videoState = {{ video_state|tojson }};
695
 
696
+ // ... (функции setupSharedVideo, playPauseVideo, seekVideo - без изменений) ...
697
+ function setupSharedVideo() {
 
698
  const video = document.getElementById('shared-video');
699
  if (!video) return;
700
 
 
771
  }
772
  }
773
 
774
+ // Обработчики событий сокетов, связанных с общим видео (без изменений)
775
  socket.on('video_url_updated', (data) => {
776
+ const videoContainer = $('#shared-video-container');
 
 
777
  videoContainer.empty();
778
 
779
  const newIframe = $('<iframe>', {
 
783
  allow: 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share',
784
  allowfullscreen: true
785
  });
 
 
786
  videoContainer.append(newIframe);
787
 
788
+ const videoControls = $('<div>', { id: 'video-controls' });
 
789
  videoControls.append($('<button>', { id: 'play-pause', 'data-state': 'paused' }).html('<i class="material-icons">play_arrow</i>'));
790
  videoControls.append($('<button>', { id: 'seek-backward' }).html('<i class="material-icons">replay_10</i>'));
791
  videoControls.append($('<button>', { id: 'seek-forward' }).html('<i class="material-icons">forward_10</i>'));
792
  videoContainer.append(videoControls);
793
 
794
  videoState = { playing: false, time: 0 };
795
+ setupSharedVideo(); // Важно!
796
  });
797
 
798
  socket.on('video_controlled', (data) => {
799
+ videoState = data.state;
800
+ if (data.action === 'play_pause') {
801
+ playPauseVideo(data.state.playing);
802
+ } else if (data.action === 'seek') {
803
+ seekVideo(data.state.time);
804
+ }
805
+ const playPauseButton = document.getElementById('play-pause');
806
  if (playPauseButton) {
807
  playPauseButton.innerHTML = `<i class="material-icons">${data.state.playing ? 'pause' : 'play_arrow'}</i>`;
808
  }
809
  });
810
 
811
+ // Функции для WebRTC (с исправлениями)
 
 
 
 
 
 
 
812
 
813
  function createControls(user, videoContainer) {
814
  const controls = document.createElement('div');
 
842
  const muteUserButton = document.createElement('button');
843
  muteUserButton.innerHTML = 'Mute';
844
  muteUserButton.title = `Mute ${targetUser}`;
845
+ muteUserButton.onclick = () => {
846
  socket.emit('admin_mute', { token, targetUser, byUser: username });
847
  };
848
+ adminControls. appendChild(muteUserButton);
849
  videoContainer.appendChild(adminControls);
850
  }
851
 
 
852
  function toggleAudio(user, button) {
853
+ const muted = button.dataset.muted === 'true';
854
+ button.dataset.muted = !muted;
855
+ button.innerHTML = `<i class="material-icons">${!muted ? 'mic' : 'mic_off'}</i>`;
856
+ if (user === username) {
857
+ localStream.getAudioTracks().forEach(track => track.enabled = !muted);
858
+ }
 
859
  }
860
 
861
  function toggleVideo(user, button) {
862
+ const videoMuted = button.dataset.videoMuted === 'true';
863
+ button.dataset.videoMuted = !videoMuted;
864
+ button.innerHTML = `<i class="material-icons">${!videoMuted ? 'videocam' : 'videocam_off'}</i>`;
 
865
  if (user === username) {
866
+ localStream.getVideoTracks().forEach(track => track.enabled = !videoMuted);
867
  }
868
  }
869
 
870
 
871
+ function addVideoStream(stream, user, isLocal = false) {
872
+ console.log('Adding video stream for', user);
 
 
 
 
 
 
 
 
 
 
873
  const existingVideo = document.querySelector(`video[data-user="${user}"]`);
874
+ if (existingVideo) {
875
+ console.log('Video already exists for', user, 'Replacing stream.');
876
+ existingVideo.srcObject = stream; // Заменяем поток, если видео уже есть
877
+ return;
878
+ }
879
 
880
+ const videoContainer = document.createElement('div');
881
  videoContainer.classList.add('video-container');
882
 
883
  const video = document.createElement('video');
884
  video.srcObject = stream;
885
+ video.dataset.user = user;
886
+ video.setAttribute('playsinline', ''); // iOS
887
+ video.setAttribute('autoplay', ''); // Важно для автоматического воспроизведения
888
+
889
+ if (isLocal) {
890
+ video.muted = true; // Локальное видео мьютим
891
+ }
892
+
893
  video.addEventListener('loadedmetadata', () => {
894
+ video.play().catch(e => console.error("Autoplay failed:", e));
895
  });
896
 
 
 
897
 
 
898
  const userIndicator = document.createElement('div');
899
  userIndicator.classList.add('user-indicator');
900
  userIndicator.textContent = user;
901
  videoContainer.appendChild(userIndicator);
 
902
 
903
+ createControls(user, videoContainer); // Создаем кнопки
904
+
905
+ videoContainer.appendChild(video);
906
+ document.getElementById('video-grid').appendChild(videoContainer);
907
  }
908
 
 
 
 
 
 
909
 
910
+ navigator.mediaDevices.getUserMedia({ video: true, audio: true })
911
+ .then(stream => {
912
+ localStream = stream;
913
+ addVideoStream(stream, username, true); // Добавляем локальное видео *сразу*
914
+ socket.emit('join', { token, username });
915
+ })
916
+ .catch(err => console.error("Error accessing media devices:", err));
917
+
918
+
919
+
920
+ function createPeerConnection(remoteUser) {
921
+ if (peers[remoteUser]) {
922
+ return peers[remoteUser].peerConnection; // Already exists
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
923
  }
924
+
925
+ const peerConnection = new RTCPeerConnection(iceConfig);
926
+ peers[remoteUser] = { peerConnection: peerConnection, iceCandidates: [] };
927
+
928
+ // Добавляем *все* треки из localStream в peerConnection
929
+ localStream.getTracks().forEach(track => {
930
+ peerConnection.addTrack(track, localStream);
931
+ });
932
+
933
+
934
+ peerConnection.ontrack = event => {
935
+ console.log('Received remote stream from', remoteUser);
936
+ addVideoStream(event.streams[0], remoteUser); // Добавляем удаленное видео
937
+ };
938
+
939
+ peerConnection.oniceconnectionstatechange = () => {
940
+ console.log(`ICE state change for ${remoteUser}: ${peerConnection.iceConnectionState}`);
941
+ if (peerConnection.iceConnectionState === 'failed' || peerConnection.iceConnectionState === 'disconnected') {
942
+ console.warn(`Connection with ${remoteUser} failed or disconnected.`);
943
+ if (peers[remoteUser]) {
944
+ peers[remoteUser].peerConnection.close();
945
+ delete peers[remoteUser];
946
+ const videoElement = document.querySelector(`video[data-user="${remoteUser}"]`);
947
+ if (videoElement) {
948
+ videoElement.parentElement.remove(); // Удаляем контейнер
949
+ }
950
+ }
951
+ }
952
+ };
953
+
954
+ peerConnection.onicecandidate = event => {
955
+ if (event.candidate) {
956
+ socket.emit('signal', {
957
+ to: remoteUser,
958
+ from: username,
959
+ token: token,
960
+ signal: { type: 'candidate', candidate: event.candidate }
961
+ });
962
+ }
963
+ };
964
+ return peerConnection;
965
  }
966
 
967
+
968
  socket.on('signal', data => {
969
+ if (data.from === username) return; // Ignore signals from self
 
970
 
971
+ let peerEntry = peers[data.from];
972
+ if (!peerEntry) {
973
+ // Если нет соединения, создаем новое
974
+ createPeerConnection(data.from);
975
+ peerEntry = peers[data.from]; // Get updated peerEntry
976
+ }
977
+ const peerConnection = peerEntry.peerConnection;
978
+ if (data.signal.type === 'offer') {
979
+ peerConnection.setRemoteDescription(new RTCSessionDescription(data.signal))
980
+ .then(() => peerConnection.createAnswer())
981
+ .then(answer => peerConnection.setLocalDescription(answer))
982
+ .then(() => {
983
+ socket.emit('signal', {
984
+ to: data.from,
985
+ from: username,
986
+ token: token,
987
+ signal: peerConnection.localDescription
988
+ });
989
+ // Add buffered candidates
990
+ while(peerEntry.iceCandidates.length > 0){
991
+ const candidate = peerEntry.iceCandidates.shift();
992
+ peerConnection.addIceCandidate(new RTCIceCandidate(candidate))
993
+ .catch(err => console.error("Failed to add buffered ICE candidate:", err));
994
+ }
995
+ })
996
+ .catch(err => console.error("Error handling offer:", err));
997
+
998
+ } else if (data.signal.type === 'answer') {
999
+ peerConnection.setRemoteDescription(new RTCSessionDescription(data.signal))
1000
+ .then(() => {
1001
+ // Add buffered candidates
1002
+ while(peerEntry.iceCandidates.length > 0){
1003
+ const candidate = peerEntry.iceCandidates.shift();
1004
+ peerConnection.addIceCandidate(new RTCIceCandidate(candidate))
1005
+ .catch(err => console.error("Failed to add buffered ICE candidate:", err));
1006
+ }
1007
+ })
1008
+ .catch(err => console.error("Error handling answer:", err));
1009
+
1010
+ } else if (data.signal.type === 'candidate') {
1011
+ if (peerConnection.remoteDescription) {
1012
+ peerConnection.addIceCandidate(new RTCIceCandidate(data.signal.candidate))
1013
+ .catch(err => console.error("Error adding ice candidate:", err));
1014
+ } else {
1015
+ // Buffer the candidate
1016
+ peerEntry.iceCandidates.push(data.signal.candidate);
1017
+ console.log("Candidate buffered for", data.from);
1018
  }
 
 
 
 
 
 
 
 
 
 
 
 
1019
  }
 
1020
  });
1021
 
1022
  socket.on('user_joined', data => {
1023
+ console.log('User joined:', data.username);
1024
+ document.getElementById('users').innerText = 'Users: ' + data.users.join(', ');
1025
 
1026
+ // Create offer for the newly joined user
1027
+ if (data.username !== username) {
1028
  const peerConnection = createPeerConnection(data.username);
1029
+
1030
  peerConnection.createOffer()
1031
  .then(offer => peerConnection.setLocalDescription(offer))
1032
  .then(() => {
1033
  socket.emit('signal', {
 
 
1034
  to: data.username,
1035
+ from: username,
1036
+ token: token,
1037
  signal: peerConnection.localDescription
1038
  });
1039
  })
1040
+ .catch(err => console.error("Error creating offer:", err));
1041
  }
1042
  });
1043
 
 
 
 
 
 
 
 
 
 
 
1044
 
1045
  socket.on('init_users', data => {
1046
+ console.log("Initial users:", data.users);
1047
+ // Create peer connections for existing users.
1048
+ data.users.forEach(user => {
1049
+ if (user !== username) {
1050
+ createPeerConnection(user);
1051
+ }
1052
+ });
1053
+ });
1054
+
1055
+ socket.on('user_left', data => {
1056
+ console.log('User left:', data.username);
1057
+ document.getElementById('users').innerText = 'Users: ' + data.users.join(', ');
1058
+ if (peers[data.username]) {
1059
+ peers[data.username].peerConnection.close();
1060
+ delete peers[data.username];
1061
+
1062
+ const videoElement = document.querySelector(`video[data-user="${data.username}"]`);
1063
+ if(videoElement){
1064
+ videoElement.parentElement.remove(); // Remove the container
1065
  }
1066
+ }
1067
  });
1068
+
1069
+ socket.on('admin_muted', data => {
1070
  if (data.targetUser === username) {
1071
  localStream.getAudioTracks().forEach(track => track.enabled = false);
 
1072
  const muteButton = document.querySelector(`.controls button[data-muted][data-user="${username}"]`);
1073
  if (muteButton) {
1074
  muteButton.dataset.muted = 'true';
 
1078
  });
1079
 
1080
  function leaveRoom() {
1081
+ socket.emit('leave', { token, username });
1082
  if (localStream) {
1083
+ localStream.getTracks().forEach(track => track.stop());
1084
  }
1085
  for (let user in peers) {
1086
  peers[user].peerConnection.close();
 
1088
  window.location.href = '/dashboard';
1089
  }
1090
 
1091
+ $(document).ready(() => {
1092
+ setupSharedVideo(); // Важно! Инициализация после загрузки DOM
1093
+ });
1094
  </script>
1095
  </body>
1096
  </html>
1097
  ''', token=token, session=session, is_admin=is_admin, video_url=video_url, video_state=video_state)
1098
 
1099
 
1100
+ # WebSocket events
 
1101
  @socketio.on('join')
1102
  def handle_join(data):
1103
  token = data['token']
1104
  username = data['username']
1105
+ print(f"User {username} joining room {token}")
1106
 
1107
  if token in rooms and len(rooms[token]['users']) < rooms[token]['max_users']:
1108
  join_room(token)
1109
  if username not in rooms[token]['users']:
1110
+ rooms[token]['users'].append(username)
1111
+ save_json(ROOMS_DB, rooms)
1112
+
1113
  emit('initial_video_state', {'state': rooms[token]['video_state']}, to=request.sid)
1114
+ # Broadcast to everyone (including the new user)
1115
  emit('user_joined', {'username': username, 'users': rooms[token]['users']}, room=token)
1116
+ # Send the list of users directly to the new user
1117
  emit('init_users', {'users': rooms[token]['users']}, to=request.sid)
 
1118
  else:
1119
+ emit('error_message', {'message': 'Room is full or does not exist'}, to=request.sid)
1120
 
1121
  @socketio.on('leave')
1122
  def handle_leave(data):
1123
  token = data['token']
1124
  username = data['username']
1125
+ print(f"User {username} leaving room {token}")
1126
 
1127
  if token in rooms and username in rooms[token]['users']:
1128
  leave_room(token)
1129
  rooms[token]['users'].remove(username)
1130
  if rooms[token]['admin'] == username:
1131
+ del rooms[token]
1132
  save_json(ROOMS_DB, rooms)
1133
  emit('user_left', {'username': username, 'users': rooms[token]['users']}, room=token)
1134
 
1135
+
1136
  @socketio.on('signal')
1137
  def handle_signal(data):
1138
+ # Send the signal to the specified recipient
1139
+ emit('signal', data, room=data['token'], include_self=False)
1140
+
1141
 
1142
  @socketio.on('admin_mute')
1143
  def handle_admin_mute(data):
 
1152
  def handle_set_video_url(data):
1153
  token = data['token']
1154
  video_url = data['video_url']
1155
+ print(f"Setting video URL for room {token} to {video_url}")
1156
 
1157
  if token in rooms and rooms[token]['admin'] == session.get('username'):
1158
+ # Sanitize the URL (basic example - prevent script injection)
1159
+ if '<script' in video_url.lower():
1160
+ print("Invalid video URL - contains script tags")
1161
+ return # Reject the URL
1162
+
1163
+ # YouTube, Rutube, or VK Video URL.
1164
+ video_url = convert_to_embed_url(video_url)
1165
+ if not video_url:
1166
+ print("Invalid video URL - not from a supported platform")
1167
+ return
1168
+
1169
  rooms[token]['video_url'] = video_url
1170
  rooms[token]['video_state'] = {'playing': False, 'time': 0}
1171
  save_json(ROOMS_DB, rooms)
 
1176
  token = data['token']
1177
  action = data['action']
1178
  if token not in rooms:
1179
+ return
1180
 
1181
  if action == 'play_pause':
1182
  rooms[token]['video_state']['playing'] = data['state']['playing']
 
1186
  save_json(ROOMS_DB, rooms)
1187
  emit('video_controlled', {'action': action, 'state': rooms[token]['video_state']}, room=token)
1188
 
1189
+
1190
+ def convert_to_embed_url(url):
1191
+ """Converts a YouTube, Rutube, or VK Video URL to an embeddable URL."""
1192
+ try:
1193
+ if "youtube.com" in url or "youtu.be" in url:
1194
+ if "watch?v=" in url:
1195
+ video_id = url.split("watch?v=")[1].split("&")[0]
1196
+ elif "youtu.be" in url:
1197
+ video_id = url.split("youtu.be/")[1].split("?")[0]
1198
+ else:
1199
+ return None # Invalid YouTube URL
1200
+ return f"https://www.youtube.com/embed/{video_id}?autoplay=1&controls=0"
1201
+
1202
+ elif "rutube.ru" in url:
1203
+ if "/video/" in url:
1204
+ video_id = url.split("/video/")[1].split("/")[0]
1205
+ elif "/play/embed/" in url: # Already embedded
1206
+ return url + "?autoplay=1" # Keep autoplay
1207
+ else:
1208
+ return None
1209
+ return f"https://rutube.ru/play/embed/{video_id}?autoplay=1"
1210
+
1211
+
1212
+ elif "vk.com" in url:
1213
+ if "video" in url and "z=" in url:
1214
+ video_id = url.split("z=video")[1].split("%")[0]
1215
+ return f"https://vk.com/video_ext.php?oid={video_id}&hd=2&autoplay=1" #hd=2 is optional
1216
+
1217
+ elif "video_ext.php" in url: # Already embedded
1218
+ return url.replace("autoplay=0", "autoplay=1") # Ensure autoplay
1219
+ else:
1220
+ return None
1221
+
1222
+ return None # Unsupported platform
1223
+
1224
+ except Exception as e:
1225
+ print(f"Error converting URL: {e}")
1226
+ return None
1227
+
1228
+
1229
  if __name__ == '__main__':
1230
  socketio.run(app, host='0.0.0.0', port=7860, debug=True, allow_unsafe_werkzeug=True)