This commit is contained in:
2026-01-07 05:26:56 +09:00
parent c21bbb779f
commit db1c4958ea
6 changed files with 301 additions and 137 deletions

333
server.py
View File

@@ -1,7 +1,7 @@
"""
KazicCAM - Серверная часть с веб-интерфейсом
FastAPI + OpenCV + WebSocket + Multiprocessing
Версия: 2.1.0, 25 пересборка
Версия: 3.1.1, 35 пересборка
Для Windows
"""
@@ -40,7 +40,10 @@ load_dotenv()
SERVER_CONFIG = {
# считываем переменную окружения HOST (или legacy 'host'), чтобы можно было управлять через .env / docker-compose
"host": os.getenv("HOST", os.getenv("host", "0.0.0.0")), # сюда IP смотрящий наружу или 0.0.0.0 для всех интерфейсов
"port": 8000,
"port": int(os.getenv("PORT", 8000)),
"ssl_certfile": os.getenv("SSL_CERTFILE", "/etc/videostream/ssl/cert.pem"),
"ssl_keyfile": os.getenv("SSL_KEYFILE", "/etc/videostream/ssl/key.pem"),
"ssl_enabled": os.getenv("SSL_ENABLED", "false").lower() == "true",
"debug": False,
"max_clients_per_room": 50,
"max_rooms": 100,
@@ -77,7 +80,7 @@ server_stats = None
stats_lock = None
cleanup_task = None
templates = None
admin_frame_queues = None
processed_video_queues = {}
# Администраторы
ADMINS = [
@@ -348,81 +351,74 @@ class VideoProcessor:
@staticmethod
def _process_video_stream(client_id: str):
"""Основной цикл обработки видео"""
print(f"\n[VideoProcessor Process] ===== PROCESS START =====")
print(f"[VideoProcessor Process] Client ID: {client_id}, PID: {os.getpid()}")
"""Основной цикл обработки видео с пересылкой в общую очередь"""
print(f"[VideoProcessor] Process started for client {client_id}")
video_queue = None
command_queue = None
# Ждем инициализации очередей
for attempt in range(100):
for _ in range(100):
try:
if client_id in video_queues and client_id in command_queues:
video_queue = video_queues[client_id]
command_queue = command_queues[client_id]
print(f"[VideoProcessor Process] ✓ Queues found on attempt {attempt+1}")
break
except:
pass
time.sleep(0.1)
if not video_queue or not command_queue:
print(f"[VideoProcessor Process] ❌ CRITICAL: Queues not found after 10s for client {client_id}")
print(f"[VideoProcessor] Queues not found for client {client_id}")
return
# Создаем очередь для обработанных кадров (для администраторов)
processed_queue = Queue(maxsize=10)
# Добавляем эту очередь в глобальный словарь для доступа администраторов
processed_video_queues[client_id] = processed_queue
frame_count = 0
last_log_time = time.time()
last_frame_time = time.time()
print(f"[VideoProcessor Process] ===== WAITING FOR FRAMES =====\n")
while True:
try:
if not video_queue.empty():
frame_data = video_queue.get(timeout=1)
frame_count += 1
last_frame_time = time.time()
try:
nparr = np.frombuffer(frame_data, np.uint8)
frame = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
nparr = np.frombuffer(frame_data, np.uint8)
frame = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
if frame is not None:
# Применяем команды из очереди
while not command_queue.empty():
try:
command = command_queue.get_nowait()
frame = VideoProcessor._apply_command(frame, command)
except:
break
if frame is not None:
# Применяем команды из очереди
commands_applied = 0
while not command_queue.empty():
try:
command = command_queue.get_nowait()
frame = VideoProcessor._apply_command(frame, command)
commands_applied += 1
except:
break
if commands_applied > 0:
print(f"[VideoProcessor Process] {client_id}: applied {commands_applied} commands to frame #{frame_count}")
else:
print(f"[VideoProcessor Process] ⚠️ {client_id}: frame decode FAILED at frame #{frame_count}")
except Exception as e:
print(f"[VideoProcessor Process] ❌ {client_id}: Error decoding frame #{frame_count}: {e}")
# Логируем прогресс каждые 5 секунд
current_time = time.time()
if current_time - last_log_time > 5:
time_since_frame = current_time - last_frame_time
if frame_count == 0:
print(f"[VideoProcessor Process] ⚠️ {client_id}: NO FRAMES YET (waiting for {time_since_frame:.1f}s)")
else:
print(f"[VideoProcessor Process] {client_id}: Processed {frame_count} frames (last {time_since_frame:.1f}s ago)")
last_log_time = current_time
# Кодируем обратно в JPEG
encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), 85]
success, encoded_frame = cv2.imencode('.jpg', frame, encode_param)
if success:
# Отправляем в очередь для администраторов
if not processed_queue.full():
processed_queue.put(encoded_frame.tobytes())
# Логируем каждые 5 секунд
current_time = time.time()
if current_time - last_log_time > 5:
print(f"[VideoProcessor] Client {client_id}: Processed {frame_count} frames")
last_log_time = current_time
time.sleep(0.001)
except Exception as e:
print(f"[VideoProcessor Process] ❌ {client_id}: Error in main loop: {type(e).__name__}: {e}")
print(f"[VideoProcessor] Error: {e}")
time.sleep(0.1)
print(f"[VideoProcessor Process] ===== PROCESS END =====\n")
@staticmethod
def _apply_command(frame: np.ndarray, command: Dict) -> np.ndarray:
@@ -560,7 +556,7 @@ async def cleanup_inactive_sessions():
async def lifespan(app: FastAPI):
"""Управление жизненным циклом приложения"""
global rooms, clients, admin_sessions, client_websockets, admin_websockets
global video_queues, command_queues, room_stats, server_stats, stats_lock, cleanup_task, templates, admin_frame_queues
global video_queues, command_queues, room_stats, server_stats, stats_lock, cleanup_task, templates
import threading
stats_lock = threading.Lock()
@@ -572,7 +568,6 @@ async def lifespan(app: FastAPI):
admin_websockets = {}
video_queues = {}
command_queues = {}
admin_frame_queues = {}
room_stats = {}
server_stats = {
"total_rooms": 0,
@@ -747,7 +742,8 @@ async def room_page(request: Request, room_id: str):
"clients": clients_list,
"username": admin_sessions[session_id]["username"],
"server_host": server_host,
"server_port": SERVER_CONFIG["port"]
"server_port": SERVER_CONFIG["port"],
"ssl_enabled": SERVER_CONFIG.get("ssl_enabled", False)
})
@app.get("/stream/{client_id}", response_class=HTMLResponse)
@@ -1079,17 +1075,6 @@ async def client_websocket_endpoint(websocket: WebSocket, room_id: str, password
print(f"[WebSocket Client] ⚠️ {client_id}: video queue is FULL, dropping frame")
else:
print(f"[WebSocket Client] ❌ {client_id}: video_queue not found!")
# Также пробуем переслать тот же байтовый кадр администраторам, если кто-то смотрит
try:
if admin_frame_queues is not None and client_id in admin_frame_queues:
aq = admin_frame_queues[client_id]
try:
aq.put_nowait(message)
except asyncio.QueueFull:
# Если очередь админа переполнена, пропускаем кадр
pass
except Exception:
pass
elif isinstance(message, str):
try:
@@ -1131,10 +1116,10 @@ async def client_websocket_endpoint(websocket: WebSocket, room_id: str, password
if video_processor:
video_processor.stop()
# Очищаем очередь админа, чтобы фреймы перестали отправляться
# Очищаем очередь для администраторов
try:
if admin_frame_queues is not None and client_id in admin_frame_queues:
del admin_frame_queues[client_id]
if client_id in processed_video_queues:
del processed_video_queues[client_id]
except Exception:
pass
print(f"[WebSocket Client] ✓ VideoProcessor stopped for {client_id}")
@@ -1143,21 +1128,21 @@ async def client_websocket_endpoint(websocket: WebSocket, room_id: str, password
@app.websocket("/ws/admin/{session_id}")
async def admin_websocket_endpoint(websocket: WebSocket, session_id: str):
"""WebSocket для администраторов (просмотр и управление)"""
global admin_sessions
if not AdminAuth.verify_session(session_id):
print(f"[WebSocket] Admin connection REJECTED: session_id={session_id} not found in admin_sessions")
print(f"[WebSocket] Available sessions: {list(admin_sessions.keys()) if admin_sessions else 'None'}")
await websocket.close(code=1008)
return
await websocket.accept()
admin_websockets[session_id] = websocket
# Словарь для хранения активных стримов
active_streams = {}
try:
await websocket.send_text(json.dumps({
"type": "connected",
"message": "Admin WebSocket connected"
"message": "Admin WebSocket connected",
"session_id": session_id
}))
print(f"[WebSocket] Admin connected: {session_id}")
@@ -1176,7 +1161,33 @@ async def admin_websocket_endpoint(websocket: WebSocket, session_id: str):
if cmd_type == "watch_client":
client_id = command.get("client_id")
await _stream_client_to_admin(client_id, session_id)
# Останавливаем предыдущий стрим если есть
if session_id in active_streams:
active_streams[session_id].cancel()
# Запускаем новый стрим
task = asyncio.create_task(
_stream_client_to_admin(client_id, session_id)
)
active_streams[session_id] = task
await websocket.send_text(json.dumps({
"type": "watching_started",
"client_id": client_id,
"message": f"Started watching client {client_id}"
}))
elif cmd_type == "stop_watching":
# Останавливаем просмотр
if session_id in active_streams:
active_streams[session_id].cancel()
del active_streams[session_id]
await websocket.send_text(json.dumps({
"type": "watching_stopped",
"message": "Stopped watching"
}))
elif cmd_type == "control_client":
client_id = command.get("client_id")
@@ -1186,7 +1197,8 @@ async def admin_websocket_endpoint(websocket: WebSocket, session_id: str):
command_queues[client_id].put(control_cmd)
await websocket.send_text(json.dumps({
"type": "control_response",
"success": True
"success": True,
"command": control_cmd
}))
elif cmd_type == "get_stats":
@@ -1214,67 +1226,99 @@ async def admin_websocket_endpoint(websocket: WebSocket, session_id: str):
except Exception as e:
print(f"[WebSocket] Admin connection error: {e}")
finally:
# Останавливаем все активные стримы
if session_id in active_streams:
active_streams[session_id].cancel()
if session_id in admin_websockets:
del admin_websockets[session_id]
async def _stream_client_to_admin(client_id: str, admin_session_id: str):
"""Потоковая передача видео от клиента к администратору"""
global admin_frame_queues, admin_websockets, clients, stats_lock
if client_id not in clients or admin_session_id not in admin_websockets:
return
try:
ws = admin_websockets[admin_session_id]
# Уведомляем админа о старте стрима
await ws.send_text(json.dumps({"type": "stream_started", "client_id": client_id}))
admin_ws = admin_websockets[admin_session_id]
await admin_ws.send_text(json.dumps({
"type": "stream_started",
"client_id": client_id,
"timestamp": datetime.now().isoformat()
}))
# Получаем информацию о клиенте
with stats_lock:
if client_id in clients:
client = clients[client_id]
await ws.send_text(json.dumps({
await admin_ws.send_text(json.dumps({
"type": "stream_info",
"message": f"Streaming from client {client_id}",
"quality": client.get("video_settings", {}).get("quality"),
"ip": client.get("ip_address"),
"connected_at": client.get("connected_at")
"client_id": client_id,
"quality": client["video_settings"]["quality"],
"frame_rate": client["video_settings"]["frame_rate"],
"resolution": client["video_settings"]["resolution"],
"ip": client["ip_address"],
"connected_at": client["connected_at"],
"status": "streaming"
}))
# Подготовим очередь для пересылки байтов администраторам
if admin_frame_queues is None:
print(f"[WebSocket] Started streaming from client {client_id} to admin {admin_session_id}")
# Ждем пока появится очередь обработанных кадров
start_time = time.time()
while client_id not in processed_video_queues and time.time() - start_time < 10:
await asyncio.sleep(0.1)
if client_id not in processed_video_queues:
await admin_ws.send_text(json.dumps({
"type": "stream_error",
"message": "No video stream available from client"
}))
return
if client_id not in admin_frame_queues:
# Создаём очередь для этого клиента, если её нет
admin_frame_queues[client_id] = asyncio.Queue(maxsize=128)
aq = admin_frame_queues[client_id]
print(f"[WebSocket] Started streaming frames to admin {admin_session_id} for client {client_id}")
# Цикл чтения кадров из очереди и отправки их админу
# Выходим только если очередь удалена (клиент отключился) или произойдёт исключение
try:
while client_id in admin_frame_queues:
try:
# Пытаемся получить кадр с таймаутом, чтобы периодически проверять наличие очереди
frame_bytes = await asyncio.wait_for(aq.get(), timeout=2.0)
try:
await ws.send_bytes(frame_bytes)
except Exception as e:
print(f"[WebSocket] Error sending bytes to admin {admin_session_id}: {e}")
break
except asyncio.TimeoutError:
# Таймаут OK — просто продолжаем ждать
continue
except asyncio.CancelledError:
break
except Exception as e:
print(f"[WebSocket] Error in admin frame loop: {e}")
finally:
print(f"[WebSocket] Stopped streaming frames to admin {admin_session_id} for client {client_id}")
processed_queue = processed_video_queues[client_id]
frame_count = 0
last_update_time = time.time()
# Основной цикл пересылки видео
while (client_id in clients and
admin_session_id in admin_websockets and
clients[client_id].get("is_streaming", False)):
try:
# Получаем обработанный кадр
if not processed_queue.empty():
frame_data = processed_queue.get_nowait()
frame_count += 1
# Отправляем кадр администратору как бинарные данные
await admin_ws.send_bytes(frame_data)
# Отправляем статистику каждую секунду
current_time = time.time()
if current_time - last_update_time > 1:
await admin_ws.send_text(json.dumps({
"type": "stream_stats",
"frames_sent": frame_count,
"timestamp": datetime.now().isoformat()
}))
last_update_time = current_time
# Небольшая задержка для CPU
await asyncio.sleep(0.01)
except asyncio.CancelledError:
# Стрим был отменен администратором
break
except Exception as e:
print(f"[WebSocket] Error streaming to admin: {e}")
break
print(f"[WebSocket] Stopped streaming from client {client_id} to admin {admin_session_id}")
except Exception as e:
print(f"[WebSocket] Error streaming to admin: {e}")
print(f"[WebSocket] Error in stream to admin: {e}")
# ========== КЛИЕНТСКИЙ API ==========
@app.post("/api/client/connect")
@@ -1288,11 +1332,12 @@ async def client_connect_api(connection_data: dict):
if client_id:
server_host = get_server_host()
ws_protocol = "wss" if SERVER_CONFIG.get("ssl_enabled", False) else "ws"
return {
"success": True,
"client_id": client_id,
"room_id": room_id,
"ws_url": f"ws://{server_host}:{SERVER_CONFIG['port']}/ws/client/{room_id}/{password}"
"ws_url": f"{ws_protocol}://{server_host}:{SERVER_CONFIG['port']}/ws/client/{room_id}/{password}"
}
return {"success": False, "error": "Connection failed"}
@@ -2497,7 +2542,7 @@ def create_html_templates():
<div class="connection-info">
<h4>Client Connection Information</h4>
<p>Clients can connect to this room using:</p>
<p><strong>WebSocket URL:</strong> <code>ws://{{ server_host }}:{{ server_port }}/ws/client/{{ room.id }}/{{ room.password }}</code></p>
<p><strong>WebSocket URL:</strong> <code>{{ 'wss' if ssl_enabled else 'ws' }}://{{ server_host }}:{{ server_port }}/ws/client/{{ room.id }}/{{ room.password }}</code></p>
<p><strong>Room ID:</strong> <code>{{ room.id }}</code></p>
<p><strong>Password:</strong> <code>{{ room.password }}</code></p>
</div>
@@ -3043,8 +3088,9 @@ def create_html_templates():
// Show room preview
previewDiv.style.display = 'block';
document.getElementById('previewId').textContent = result.room.id;
const wsProto = window.location.protocol === 'https:' ? 'wss' : 'ws';
document.getElementById('previewWsUrl').textContent =
`ws://${window.location.hostname}:{{ server_port }}/ws/client/${result.room.id}/${result.room.password}`;
`${wsProto}://${window.location.hostname}:{{ server_port }}/ws/client/${result.room.id}/${result.room.password}`;
// Clear form
this.reset();
@@ -3567,7 +3613,8 @@ def create_html_templates():
return;
}
const wsUrl = `ws://${window.location.hostname}:{{ server_port }}/ws/admin/${sessionId}`;
const wsProto = window.location.protocol === 'https:' ? 'wss' : 'ws';
const wsUrl = `${wsProto}://${window.location.hostname}:{{ server_port }}/ws/admin/${sessionId}`;
try {
ws = new WebSocket(wsUrl);
@@ -3882,31 +3929,55 @@ def main():
create_html_templates()
server_host = get_server_host()
ssl_enabled = SERVER_CONFIG.get("ssl_enabled", False)
protocol = "https" if ssl_enabled else "http"
ws_protocol = "wss" if ssl_enabled else "ws"
print("=" * 60)
print("🎥 Video Streaming Server with Web Interface")
print("=" * 60)
print(f"🌐 Web Interface: http://{server_host}:{SERVER_CONFIG['port']}")
print(f"🔌 WebSocket: ws://{server_host}:{SERVER_CONFIG['port']}")
print(f"👤 Admin Login: http://{server_host}:{SERVER_CONFIG['port']}/")
print(f"🌐 Web Interface: {protocol}://{server_host}:{SERVER_CONFIG['port']}")
print(f"🔌 WebSocket: {ws_protocol}://{server_host}:{SERVER_CONFIG['port']}")
print(f"👤 Admin Login: {protocol}://{server_host}:{SERVER_CONFIG['port']}/")
print("=" * 60)
print("Default Admin Accounts:")
print(" • admin / admin123")
print(" • administrator / securepass")
print(" • supervisor / superpass")
print("=" * 60)
if ssl_enabled:
print("🔒 SSL/TLS enabled")
print(f" Certificate: {SERVER_CONFIG.get('ssl_certfile')}")
print(f" Key: {SERVER_CONFIG.get('ssl_keyfile')}")
print("=" * 60)
print("Press Ctrl+C to stop the server")
try:
uvicorn.run(
"server:app",
host=SERVER_CONFIG["host"],
port=SERVER_CONFIG["port"],
reload=False,
log_level="info",
ws_ping_interval=SERVER_CONFIG["websocket_ping_interval"],
ws_ping_timeout=SERVER_CONFIG["websocket_ping_timeout"]
)
uvicorn_kwargs = {
"host": SERVER_CONFIG["host"],
"port": SERVER_CONFIG["port"],
"log_level": "info",
"ws_ping_interval": SERVER_CONFIG["websocket_ping_interval"],
"ws_ping_timeout": SERVER_CONFIG["websocket_ping_timeout"],
}
# Добавляем SSL если включено
if ssl_enabled:
ssl_certfile = SERVER_CONFIG.get("ssl_certfile")
ssl_keyfile = SERVER_CONFIG.get("ssl_keyfile")
if ssl_certfile and ssl_keyfile:
import os
if os.path.exists(ssl_certfile) and os.path.exists(ssl_keyfile):
uvicorn_kwargs["ssl_certfile"] = ssl_certfile
uvicorn_kwargs["ssl_keyfile"] = ssl_keyfile
else:
print(f"⚠️ SSL files not found, running without SSL")
uvicorn.run("server:app", **uvicorn_kwargs)
except KeyboardInterrupt:
print("\nServer stopped by user")
except Exception as e:

20
ssl/cert.pem Normal file
View File

@@ -0,0 +1,20 @@
-----BEGIN CERTIFICATE-----
MIIDMzCCAhugAwIBAgIURRdCcy0W7tenvTyEwUpE0XVQoxUwDQYJKoZIhvcNAQEL
BQAwGDEWMBQGA1UEAwwNMTkyLjE2OC4wLjExMjAeFw0yNjAxMDQyMDMzNDhaFw0y
NzAxMDQyMDMzNDhaMBgxFjAUBgNVBAMMDTE5Mi4xNjguMC4xMTIwggEiMA0GCSqG
SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDImBb7et5zyewUAQxQyN0iRxTI9BjWMuJk
70DCt/4CoaVUIuAVAYMsgMH4Xvh38Ur4FXsb0EYMIfOrIn4VVPZ2dwaBE2Qxwmh9
4Ku3W616jhTP1onDlOsj8ujgnAMFEDALxdTYm4rMN0a4UVUs25oI6osK5xs6rXkK
tWHUliqAyu4HYMKDYOxQlmV+dsx5LLuDR5FiNROBSegFZYlhlNg4RjKGBnGnmwg2
L/nMwiVAzpwmDU0BrX3Bs9397wQ9b+mhoOgVnzQdQmlQXrAc7wbApDTV28TjKqem
n5Kt8etPfXuM2f+X8WTAn1w8+TBGWzhWhZrH+eLz93N88dlGh6efAgMBAAGjdTBz
MB0GA1UdDgQWBBRSHgCja3iu6yWa4vnHkmp0Hnw5GjAfBgNVHSMEGDAWgBRSHgCj
a3iu6yWa4vnHkmp0Hnw5GjAPBgNVHRMBAf8EBTADAQH/MCAGA1UdEQQZMBeHBMCo
AHCHBH8AAAGCCWxvY2FsaG9zdDANBgkqhkiG9w0BAQsFAAOCAQEAMUuAoG5l7yct
hqFr5WLRhG9S6AvMDRPL8pCbL6K9ncBNVUjCOSkWv06KGuSC4+pKRQhYt4cTdclf
iKZINxDfW6Nt0Jbn+wpvcuZyEWNWaDzB3iw8Xxa0PCUQjUIri1lNfIQMsIUfFzNz
lJdC6eM04TEEjyzbAVRm677Z+q4qZyDcBvu1aazJ/8QRDJv7dM7c6agzfmflwp/q
tHr5tJGR5AC/Nvpa5vZS4qbZC+RwqADr9NSJg4X8A0VSmPLhmC0Aj9oXbaoNXD1R
GET2nGCztqjuFniXMJm9FUxepO5qEadQGq09XedwSrO1G9+hty8kL716gupXvYzd
4WTp+i4Juw==
-----END CERTIFICATE-----

28
ssl/key.pem Normal file
View File

@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDImBb7et5zyewU
AQxQyN0iRxTI9BjWMuJk70DCt/4CoaVUIuAVAYMsgMH4Xvh38Ur4FXsb0EYMIfOr
In4VVPZ2dwaBE2Qxwmh94Ku3W616jhTP1onDlOsj8ujgnAMFEDALxdTYm4rMN0a4
UVUs25oI6osK5xs6rXkKtWHUliqAyu4HYMKDYOxQlmV+dsx5LLuDR5FiNROBSegF
ZYlhlNg4RjKGBnGnmwg2L/nMwiVAzpwmDU0BrX3Bs9397wQ9b+mhoOgVnzQdQmlQ
XrAc7wbApDTV28TjKqemn5Kt8etPfXuM2f+X8WTAn1w8+TBGWzhWhZrH+eLz93N8
8dlGh6efAgMBAAECggEANp7/ZxQCfeoGYD4W0eqYGGzdkoixOKZbcluZuUvrnWDR
yZLDjMTAKL6Y0t5dbx+jp+EFiEHSyRv3o2p9haaAhCqN+VjD6C7FUD181K1glCYb
Mar8EWc8DipnUj35LohFZPdGKYNVLJ2Cos720AOuMm5XVS0wA27YLlvMm1wWj8H2
qfa6hYdf2gXGzNS7RAUc/qztnrGHEGRyEVktLgJWngKLNwQDQyt9Y2jn94lkrkWv
cwPpA3YzBpdKgcxxfqyRKCiQAYHQ5DWKI4I5EcgolYK6pnaGDLz3WtCSDAT+iffo
pmVKcWbXfEkFlK+8Mw3svSrfIThRQYCStP1cR2oc7QKBgQDvw+IVZIgv/FdUxEO8
uqVP9Jba7LuHShC7Fgho6Wy54j6VeALf3BuPSFBdW37RnYbjvG5+DRohIu0w0Tzc
/ub6ydGbw/oqO5XTgEUxt/9uRaWVG1XdN2RuARo0ycPRtXceyfXctUvqcH6S7Tby
HAr7ZxbyRZRn9R4O5YRgfJWjZQKBgQDWLTX7PAMgvalNae/PIRGm2YnL9fifUVTt
W/9k1i7dvbkKb+zFUEoMqLmC+Ru6/I2fQU/Z5xhZvzpXdbbnkhwrP4CC1mKBnVr7
3XMuBGcb0vaH5fGnGUsvCpPvu2j1SXR2E5PTfPvlU9fp9WZn0Bf+LExKKJ3k1o44
ZIAqygpIswKBgQDGWe3KNt33nLF+vdMv6dTi3XyDZn8JQDuxGlwgtqMs8D9IFf2C
xqvCEFfzs8KplMFH29Yoz5wDN8qzrRXF2daqOJYAX9OwZpTfYOldZVOaHWILhY62
MKIT4zOT4irubUo7nWaZjR5dt2zvDfF7v7lSHAm+qdNJYV3ZgwTNcaO1hQKBgANh
5WdZVEdRF3pkgOgJgqja6KUy9kE13Jx/BnKGO2k/FMwIZnnbQcAfbgaWfoyebnIk
aulrD+Ri873r2v6fPRilwRbP4fBgFs7BeY8xfJtg4onU5NMZwCk3Bo0TrZ2qEk/H
EV+Wqre9cjx8pqhfDfHyqyXErHGvvcFAPRHv7OenAoGAbHHxJGs8UZNpDrdhVWp/
M1tiEanPifTw8bZJTCFDzbD2zz8ytX4Xozn74MDkY9kCocLY24u5cHRHIqPk9Aq7
SxjwHXgaWmO+KR5kdxp4N5SHv2x4In1ycW5OgeMFcAIqkMquBQ1i7aSfWm2KcnP8
IFeNsTlZxOeCN23l5h1/mgw=
-----END PRIVATE KEY-----

View File

@@ -412,8 +412,9 @@
// Show room preview
previewDiv.style.display = 'block';
document.getElementById('previewId').textContent = result.room.id;
const wsProto = window.location.protocol === 'https:' ? 'wss' : 'ws';
document.getElementById('previewWsUrl').textContent =
`ws://${window.location.hostname}:{{ server_port }}/ws/client/${result.room.id}/${result.room.password}`;
`${wsProto}://${window.location.hostname}:{{ server_port }}/ws/client/${result.room.id}/${result.room.password}`;
// Clear form
this.reset();

View File

@@ -422,7 +422,7 @@
<div class="connection-info">
<h4>Client Connection Information</h4>
<p>Clients can connect to this room using:</p>
<p><strong>WebSocket URL:</strong> <code>ws://{{ server_host }}:{{ server_port }}/ws/client/{{ room.id }}/{{ room.password }}</code></p>
<p><strong>WebSocket URL:</strong> <code>{{ 'wss' if ssl_enabled else 'ws' }}://{{ server_host }}:{{ server_port }}/ws/client/{{ room.id }}/{{ room.password }}</code></p>
<p><strong>Room ID:</strong> <code>{{ room.id }}</code></p>
<p><strong>Password:</strong> <code>{{ room.password }}</code></p>
</div>

View File

@@ -350,6 +350,9 @@
<div class="main-content">
<!-- Video Container -->
<div class="video-container">
<!-- Canvas для отображения видеокадров -->
<canvas id="videoCanvas" style="width: 100%; height: auto; display: none; background: #000;"></canvas>
<div class="video-placeholder" id="videoPlaceholder">
<i class="fas fa-video"></i>
<h3>Waiting for Video Stream</h3>
@@ -481,7 +484,8 @@
return;
}
const wsUrl = `ws://${window.location.hostname}:{{ server_port }}/ws/admin/${sessionId}`;
const wsProto = window.location.protocol === 'https:' ? 'wss' : 'ws';
const wsUrl = `${wsProto}://${window.location.hostname}:{{ server_port }}/ws/admin/${sessionId}`;
try {
ws = new WebSocket(wsUrl);
@@ -497,10 +501,17 @@
ws.onmessage = function(event) {
try {
const data = JSON.parse(event.data);
handleWebSocketMessage(data);
// Проверяем, это бинарные данные (видеокадр) или текст (JSON)
if (event.data instanceof Blob) {
// Это видеокадр - отображаем на canvas
handleVideoFrame(event.data);
} else if (typeof event.data === 'string') {
// Это текстовое сообщение JSON
const data = JSON.parse(event.data);
handleWebSocketMessage(data);
}
} catch (error) {
console.error('Error parsing WebSocket message:', error);
console.error('Error processing WebSocket message:', error);
}
};
@@ -659,6 +670,39 @@
}
}
// Handle video frame from admin WebSocket
function handleVideoFrame(blob) {
const canvas = document.getElementById('videoCanvas');
const placeholder = document.getElementById('videoPlaceholder');
// Show canvas, hide placeholder
if (canvas.style.display === 'none') {
canvas.style.display = 'block';
placeholder.style.display = 'none';
}
// Create image from blob and draw on canvas
const img = new Image();
img.onload = function() {
const ctx = canvas.getContext('2d');
// Set canvas size to match image
canvas.width = img.width;
canvas.height = img.height;
// Draw image
ctx.drawImage(img, 0, 0);
// Revoke the object URL to free memory
URL.revokeObjectURL(img.src);
};
img.onerror = function() {
console.error('Failed to load image from blob');
URL.revokeObjectURL(img.src);
};
// Create object URL from blob and load as image
img.src = URL.createObjectURL(blob);
}
// Initialize sliders
function initializeSliders() {
// Quality slider