Spaces:
Sleeping
Sleeping
Create app.py
Browse files
app.py
ADDED
@@ -0,0 +1,236 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from flask import Flask, render_template, request, redirect, url_for, jsonify
|
2 |
+
import random
|
3 |
+
import string
|
4 |
+
import json
|
5 |
+
from flask_socketio import SocketIO, join_room, leave_room
|
6 |
+
|
7 |
+
app = Flask(__name__)
|
8 |
+
app.config['SECRET_KEY'] = 'your-secret-key-here'
|
9 |
+
socketio = SocketIO(app)
|
10 |
+
|
11 |
+
# Хранилище комнат и пользователей
|
12 |
+
rooms = {}
|
13 |
+
|
14 |
+
# Генерация 15-значного токена
|
15 |
+
def generate_token():
|
16 |
+
return ''.join(random.choices(string.ascii_letters + string.digits, k=15))
|
17 |
+
|
18 |
+
# Главная страница
|
19 |
+
@app.route('/')
|
20 |
+
def index():
|
21 |
+
return render_template('index.html')
|
22 |
+
|
23 |
+
# Создание новой комнаты
|
24 |
+
@app.route('/create', methods=['POST'])
|
25 |
+
def create_room():
|
26 |
+
token = generate_token()
|
27 |
+
rooms[token] = {
|
28 |
+
'users': [],
|
29 |
+
'max_users': 5
|
30 |
+
}
|
31 |
+
return redirect(url_for('room', token=token))
|
32 |
+
|
33 |
+
# Вход в существующую комнату
|
34 |
+
@app.route('/join', methods=['POST'])
|
35 |
+
def join():
|
36 |
+
token = request.form.get('token')
|
37 |
+
if token in rooms and len(rooms[token]['users']) < rooms[token]['max_users']:
|
38 |
+
return redirect(url_for('room', token=token))
|
39 |
+
return "Комната не найдена или переполнена", 404
|
40 |
+
|
41 |
+
# Страница комнаты
|
42 |
+
@app.route('/room/<token>')
|
43 |
+
def room(token):
|
44 |
+
if token not in rooms:
|
45 |
+
return redirect(url_for('index'))
|
46 |
+
return render_template('room.html', token=token)
|
47 |
+
|
48 |
+
# WebSocket события
|
49 |
+
@socketio.on('join')
|
50 |
+
def handle_join(data):
|
51 |
+
token = data['token']
|
52 |
+
username = data['username']
|
53 |
+
|
54 |
+
if token in rooms and len(rooms[token]['users']) < rooms[token]['max_users']:
|
55 |
+
join_room(token)
|
56 |
+
rooms[token]['users'].append(username)
|
57 |
+
socketio.emit('user_joined', {
|
58 |
+
'username': username,
|
59 |
+
'users': rooms[token]['users']
|
60 |
+
}, room=token)
|
61 |
+
|
62 |
+
@socketio.on('leave')
|
63 |
+
def handle_leave(data):
|
64 |
+
token = data['token']
|
65 |
+
username = data['username']
|
66 |
+
|
67 |
+
if token in rooms and username in rooms[token]['users']:
|
68 |
+
leave_room(token)
|
69 |
+
rooms[token]['users'].remove(username)
|
70 |
+
socketio.emit('user_left', {
|
71 |
+
'username': username,
|
72 |
+
'users': rooms[token]['users']
|
73 |
+
}, room=token)
|
74 |
+
|
75 |
+
if len(rooms[token]['users']) == 0:
|
76 |
+
del rooms[token]
|
77 |
+
|
78 |
+
@socketio.on('signal')
|
79 |
+
def handle_signal(data):
|
80 |
+
socketio.emit('signal', data, room=data['token'], skip_sid=request.sid)
|
81 |
+
|
82 |
+
# Шаблоны HTML
|
83 |
+
@app.route('/templates/index.html')
|
84 |
+
def index_template():
|
85 |
+
return '''<!DOCTYPE html>
|
86 |
+
<html>
|
87 |
+
<head>
|
88 |
+
<title>Видеоконференция</title>
|
89 |
+
<style>
|
90 |
+
body { font-family: Arial, sans-serif; text-align: center; padding: 50px; }
|
91 |
+
button, input { padding: 10px; margin: 5px; }
|
92 |
+
</style>
|
93 |
+
</head>
|
94 |
+
<body>
|
95 |
+
<h1>Видеоконференция</h1>
|
96 |
+
<form action="/create" method="post">
|
97 |
+
<button type="submit">Создать комнату</button>
|
98 |
+
</form>
|
99 |
+
<form action="/join" method="post">
|
100 |
+
<input type="text" name="token" placeholder="Введите токен комнаты" required>
|
101 |
+
<button type="submit">Войти в комнату</button>
|
102 |
+
</form>
|
103 |
+
</body>
|
104 |
+
</html>'''
|
105 |
+
|
106 |
+
@app.route('/templates/room.html')
|
107 |
+
def room_template():
|
108 |
+
return '''<!DOCTYPE html>
|
109 |
+
<html>
|
110 |
+
<head>
|
111 |
+
<title>Комната {{ token }}</title>
|
112 |
+
<style>
|
113 |
+
.video-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 10px; padding: 20px; }
|
114 |
+
video { width: 100%; background: black; }
|
115 |
+
#users { margin: 10px; }
|
116 |
+
</style>
|
117 |
+
</head>
|
118 |
+
<body>
|
119 |
+
<h1>Комната: {{ token }}</h1>
|
120 |
+
<div id="users"></div>
|
121 |
+
<div class="video-grid" id="video-grid"></div>
|
122 |
+
<button onclick="leaveRoom()">Покинуть комнату</button>
|
123 |
+
|
124 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.5.1/socket.io.js"></script>
|
125 |
+
<script>
|
126 |
+
const socket = io.connect('http://localhost:7860');
|
127 |
+
const token = '{{ token }}';
|
128 |
+
let localStream;
|
129 |
+
const peers = {};
|
130 |
+
const username = 'User_' + Math.random().toString(36).substr(2, 5);
|
131 |
+
|
132 |
+
// Получение видео/аудио потока
|
133 |
+
navigator.mediaDevices.getUserMedia({ video: true, audio: true })
|
134 |
+
.then(stream => {
|
135 |
+
localStream = stream;
|
136 |
+
addVideoStream(stream, username, true);
|
137 |
+
socket.emit('join', { token: token, username: username });
|
138 |
+
});
|
139 |
+
|
140 |
+
// Добавление видео в сетку
|
141 |
+
function addVideoStream(stream, user, muted = false) {
|
142 |
+
const video = document.createElement('video');
|
143 |
+
video.srcObject = stream;
|
144 |
+
video.addEventListener('loadedmetadata', () => {
|
145 |
+
video.play();
|
146 |
+
});
|
147 |
+
if (muted) video.muted = true;
|
148 |
+
video.dataset.user = user;
|
149 |
+
document.getElementById('video-grid').appendChild(video);
|
150 |
+
}
|
151 |
+
|
152 |
+
// Создание peer соединения
|
153 |
+
function connectToNewUser(user) {
|
154 |
+
const peer = new RTCPeerConnection();
|
155 |
+
peers[user] = peer;
|
156 |
+
|
157 |
+
localStream.getTracks().forEach(track => {
|
158 |
+
peer.addTrack(track, localStream);
|
159 |
+
});
|
160 |
+
|
161 |
+
peer.ontrack = event => {
|
162 |
+
addVideoStream(event.streams[0], user);
|
163 |
+
};
|
164 |
+
|
165 |
+
peer.onicecandidate = event => {
|
166 |
+
if (event.candidate) {
|
167 |
+
socket.emit('signal', { token: token, to: user, signal: { type: 'candidate', candidate: event.candidate } });
|
168 |
+
}
|
169 |
+
};
|
170 |
+
|
171 |
+
peer.createOffer()
|
172 |
+
.then(offer => peer.setLocalDescription(offer))
|
173 |
+
.then(() => {
|
174 |
+
socket.emit('signal', { token: token, to: user, signal: peer.localDescription });
|
175 |
+
});
|
176 |
+
}
|
177 |
+
|
178 |
+
// Обработка сигналов
|
179 |
+
socket.on('signal', data => {
|
180 |
+
const peer = peers[data.from] || new RTCPeerConnection();
|
181 |
+
peers[data.from] = peer;
|
182 |
+
|
183 |
+
if (data.signal.type === 'offer') {
|
184 |
+
peer.setRemoteDescription(new RTCSessionDescription(data.signal));
|
185 |
+
localStream.getTracks().forEach(track => peer.addTrack(track, localStream));
|
186 |
+
|
187 |
+
peer.ontrack = event => {
|
188 |
+
addVideoStream(event.streams[0], data.from);
|
189 |
+
};
|
190 |
+
|
191 |
+
peer.onicecandidate = event => {
|
192 |
+
if (event.candidate) {
|
193 |
+
socket.emit('signal', { token: token, to: data.from, signal: { type: 'candidate', candidate: event.candidate } });
|
194 |
+
}
|
195 |
+
};
|
196 |
+
|
197 |
+
peer.createAnswer()
|
198 |
+
.then(answer => peer.setLocalDescription(answer))
|
199 |
+
.then(() => {
|
200 |
+
socket.emit('signal', { token: token, to: data.from, signal: peer.localDescription });
|
201 |
+
});
|
202 |
+
} else if (data.signal.type === 'answer') {
|
203 |
+
peer.setRemoteDescription(new RTCSessionDescription(data.signal));
|
204 |
+
} else if (data.signal.type === 'candidate') {
|
205 |
+
peer.addIceCandidate(new RTCIceCandidate(data.signal.candidate));
|
206 |
+
}
|
207 |
+
});
|
208 |
+
|
209 |
+
// Обновление списка пользователей
|
210 |
+
socket.on('user_joined', data => {
|
211 |
+
document.getElementById('users').innerText = 'Пользователи: ' + data.users.join(', ');
|
212 |
+
if (data.username !== username) {
|
213 |
+
connectToNewUser(data.username);
|
214 |
+
}
|
215 |
+
});
|
216 |
+
|
217 |
+
socket.on('user_left', data => {
|
218 |
+
document.getElementById('users').innerText = 'Пользователи: ' + data.users.join(', ');
|
219 |
+
if (peers[data.username]) {
|
220 |
+
peers[data.username].close();
|
221 |
+
delete peers[data.username];
|
222 |
+
document.querySelector(`video[data-user="${data.username}"]`).remove();
|
223 |
+
}
|
224 |
+
});
|
225 |
+
|
226 |
+
function leaveRoom() {
|
227 |
+
socket.emit('leave', { token: token, username: username });
|
228 |
+
localStream.getTracks().forEach(track => track.stop());
|
229 |
+
window.location.href = '/';
|
230 |
+
}
|
231 |
+
</script>
|
232 |
+
</body>
|
233 |
+
</html>'''
|
234 |
+
|
235 |
+
if __name__ == '__main__':
|
236 |
+
socketio.run(app, host='0.0.0.0', port=7860, debug=True)
|