Spaces:
Sleeping
Sleeping
Update app.py
Browse files
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 |
-
# Загрузка
|
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 |
-
|
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 |
-
|
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('''
|
|
|
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 |
-
|
664 |
-
|
665 |
-
|
666 |
-
|
667 |
-
|
668 |
-
|
669 |
-
|
670 |
-
|
671 |
-
|
672 |
-
|
673 |
-
|
674 |
-
|
675 |
<input type="text" id="video-url-input" placeholder="URL видео (YouTube, Rutube, VK)">
|
676 |
<button id="set-video-url">Установить</button>
|
677 |
-
|
678 |
-
|
679 |
-
|
680 |
-
|
681 |
<button class="leave-button" onclick="leaveRoom()">Покинуть комнату</button>
|
682 |
</div>
|
683 |
-
|
|
|
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 |
-
|
811 |
-
|
812 |
-
|
813 |
-
|
814 |
-
|
815 |
-
|
816 |
-
|
817 |
if (playPauseButton) {
|
818 |
playPauseButton.innerHTML = `<i class="material-icons">${data.state.playing ? 'pause' : 'play_arrow'}</i>`;
|
819 |
}
|
820 |
});
|
821 |
|
822 |
-
|
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.
|
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 |
-
|
873 |
-
|
874 |
-
|
875 |
-
|
876 |
-
|
877 |
-
|
878 |
-
}
|
879 |
}
|
880 |
|
881 |
function toggleVideo(user, button) {
|
882 |
-
const
|
883 |
-
button.dataset.videoMuted = !
|
884 |
-
button.innerHTML = `<i class="material-icons">${!
|
885 |
-
|
886 |
if (user === username) {
|
887 |
-
localStream.getVideoTracks().forEach(track => track.enabled =
|
888 |
}
|
889 |
}
|
890 |
|
891 |
|
892 |
-
|
893 |
-
|
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)
|
|
|
|
|
|
|
|
|
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.
|
913 |
-
video.setAttribute('
|
|
|
|
|
|
|
|
|
|
|
|
|
914 |
video.addEventListener('loadedmetadata', () => {
|
915 |
-
video.play().catch(e => console.error(
|
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
|
929 |
-
|
|
|
|
|
930 |
}
|
931 |
|
932 |
-
// Создание соединения
|
933 |
-
function createPeerConnection(user) {
|
934 |
-
if (peers[user]) {
|
935 |
-
return peers[user].peerConnection; // Return existing connection
|
936 |
-
}
|
937 |
|
938 |
-
|
939 |
-
|
940 |
-
|
941 |
-
|
942 |
-
|
943 |
-
|
944 |
-
|
945 |
-
|
946 |
-
|
947 |
-
|
948 |
-
|
949 |
-
|
950 |
-
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
975 |
}
|
976 |
|
977 |
-
|
978 |
socket.on('signal', data => {
|
979 |
-
|
980 |
-
console.log('Получен сигнал от', data.from, ':', data.signal.type);
|
981 |
|
982 |
-
|
983 |
-
|
984 |
-
|
985 |
-
|
986 |
-
|
987 |
-
|
988 |
-
|
989 |
-
|
990 |
-
|
991 |
-
|
992 |
-
|
993 |
-
|
994 |
-
|
995 |
-
|
996 |
-
|
997 |
-
|
998 |
-
|
999 |
-
|
1000 |
-
|
1001 |
-
|
1002 |
-
|
1003 |
-
|
1004 |
-
|
1005 |
-
|
1006 |
-
|
1007 |
-
|
1008 |
-
|
1009 |
-
|
1010 |
-
|
1011 |
-
|
1012 |
-
|
1013 |
-
|
1014 |
-
|
1015 |
-
|
1016 |
-
|
1017 |
-
|
1018 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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('
|
1038 |
-
document.getElementById('users').innerText = '
|
1039 |
|
1040 |
-
|
|
|
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(
|
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 |
-
|
1069 |
-
|
1070 |
-
|
1071 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1072 |
}
|
1073 |
-
}
|
1074 |
});
|
1075 |
-
|
|
|
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
|
1089 |
if (localStream) {
|
1090 |
-
|
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 |
-
|
1115 |
-
|
|
|
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': '
|
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 |
-
|
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 |
-
|
|
|
|
|
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 |
-
|
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)
|