Compare commits

...

4 Commits

Author SHA1 Message Date
db1c4958ea sd 2026-01-07 05:26:56 +09:00
c21bbb779f fix 2025-12-09 21:13:54 +09:00
a30239a4fe some fixes 2025-12-09 20:40:48 +09:00
52abd9d1f7 ыукмук мшвущ ашч 2025-12-09 20:40:08 +09:00
7 changed files with 468 additions and 139 deletions

385
server.py
View File

@@ -1,7 +1,7 @@
""" """
KazicCAM - Серверная часть с веб-интерфейсом KazicCAM - Серверная часть с веб-интерфейсом
FastAPI + OpenCV + WebSocket + Multiprocessing FastAPI + OpenCV + WebSocket + Multiprocessing
Версия: 2.1.0, 25 пересборка Версия: 3.1.1, 35 пересборка
Для Windows Для Windows
""" """
@@ -40,7 +40,10 @@ load_dotenv()
SERVER_CONFIG = { SERVER_CONFIG = {
# считываем переменную окружения HOST (или legacy 'host'), чтобы можно было управлять через .env / docker-compose # считываем переменную окружения HOST (или legacy 'host'), чтобы можно было управлять через .env / docker-compose
"host": os.getenv("HOST", os.getenv("host", "0.0.0.0")), # сюда IP смотрящий наружу или 0.0.0.0 для всех интерфейсов "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, "debug": False,
"max_clients_per_room": 50, "max_clients_per_room": 50,
"max_rooms": 100, "max_rooms": 100,
@@ -77,7 +80,7 @@ server_stats = None
stats_lock = None stats_lock = None
cleanup_task = None cleanup_task = None
templates = None templates = None
admin_frame_queues = None processed_video_queues = {}
# Администраторы # Администраторы
ADMINS = [ ADMINS = [
@@ -348,81 +351,74 @@ class VideoProcessor:
@staticmethod @staticmethod
def _process_video_stream(client_id: str): def _process_video_stream(client_id: str):
"""Основной цикл обработки видео""" """Основной цикл обработки видео с пересылкой в общую очередь"""
print(f"\n[VideoProcessor Process] ===== PROCESS START =====") print(f"[VideoProcessor] Process started for client {client_id}")
print(f"[VideoProcessor Process] Client ID: {client_id}, PID: {os.getpid()}")
video_queue = None video_queue = None
command_queue = None command_queue = None
# Ждем инициализации очередей # Ждем инициализации очередей
for attempt in range(100): for _ in range(100):
try: try:
if client_id in video_queues and client_id in command_queues: if client_id in video_queues and client_id in command_queues:
video_queue = video_queues[client_id] video_queue = video_queues[client_id]
command_queue = command_queues[client_id] command_queue = command_queues[client_id]
print(f"[VideoProcessor Process] ✓ Queues found on attempt {attempt+1}")
break break
except: except:
pass pass
time.sleep(0.1) time.sleep(0.1)
if not video_queue or not command_queue: 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 return
# Создаем очередь для обработанных кадров (для администраторов)
processed_queue = Queue(maxsize=10)
# Добавляем эту очередь в глобальный словарь для доступа администраторов
processed_video_queues[client_id] = processed_queue
frame_count = 0 frame_count = 0
last_log_time = time.time() last_log_time = time.time()
last_frame_time = time.time()
print(f"[VideoProcessor Process] ===== WAITING FOR FRAMES =====\n")
while True: while True:
try: try:
if not video_queue.empty(): if not video_queue.empty():
frame_data = video_queue.get(timeout=1) frame_data = video_queue.get(timeout=1)
frame_count += 1 frame_count += 1
last_frame_time = time.time()
try: nparr = np.frombuffer(frame_data, np.uint8)
nparr = np.frombuffer(frame_data, np.uint8) frame = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
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: # Кодируем обратно в JPEG
# Применяем команды из очереди encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), 85]
commands_applied = 0 success, encoded_frame = cv2.imencode('.jpg', frame, encode_param)
while not command_queue.empty():
try: if success:
command = command_queue.get_nowait() # Отправляем в очередь для администраторов
frame = VideoProcessor._apply_command(frame, command) if not processed_queue.full():
commands_applied += 1 processed_queue.put(encoded_frame.tobytes())
except:
break # Логируем каждые 5 секунд
current_time = time.time()
if commands_applied > 0: if current_time - last_log_time > 5:
print(f"[VideoProcessor Process] {client_id}: applied {commands_applied} commands to frame #{frame_count}") print(f"[VideoProcessor] Client {client_id}: Processed {frame_count} frames")
else: last_log_time = current_time
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
time.sleep(0.001) time.sleep(0.001)
except Exception as e: 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) time.sleep(0.1)
print(f"[VideoProcessor Process] ===== PROCESS END =====\n")
@staticmethod @staticmethod
def _apply_command(frame: np.ndarray, command: Dict) -> np.ndarray: def _apply_command(frame: np.ndarray, command: Dict) -> np.ndarray:
@@ -572,7 +568,6 @@ async def lifespan(app: FastAPI):
admin_websockets = {} admin_websockets = {}
video_queues = {} video_queues = {}
command_queues = {} command_queues = {}
admin_frame_queues = {}
room_stats = {} room_stats = {}
server_stats = { server_stats = {
"total_rooms": 0, "total_rooms": 0,
@@ -660,11 +655,13 @@ async def login_form(request: Request, username: str = Form(...), password: str
"last_activity": datetime.now().isoformat(), "last_activity": datetime.now().isoformat(),
"is_authenticated": True "is_authenticated": True
} }
print(f"[Auth] ✓ Admin logged in: {username}, session_id={session_id[:16]}..., total sessions: {len(admin_sessions)}")
response = RedirectResponse(url="/dashboard", status_code=303) response = RedirectResponse(url="/dashboard", status_code=303)
response.set_cookie(key="session_id", value=session_id) response.set_cookie(key="session_id", value=session_id)
return response return response
print(f"[Auth] ❌ Failed login attempt: {username}")
server_host = get_server_host() server_host = get_server_host()
return templates.TemplateResponse("login.html", { return templates.TemplateResponse("login.html", {
"request": request, "request": request,
@@ -745,7 +742,8 @@ async def room_page(request: Request, room_id: str):
"clients": clients_list, "clients": clients_list,
"username": admin_sessions[session_id]["username"], "username": admin_sessions[session_id]["username"],
"server_host": server_host, "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) @app.get("/stream/{client_id}", response_class=HTMLResponse)
@@ -1064,6 +1062,9 @@ async def client_websocket_endpoint(websocket: WebSocket, room_id: str, password
message = data.get("text") or data.get("bytes") message = data.get("text") or data.get("bytes")
if isinstance(message, bytes): if isinstance(message, bytes):
if frame_count == 0:
print(f"[WebSocket Client] ✓ FIRST FRAME RECEIVED from {client_id}! Size: {len(message)} bytes")
if client_id in video_queues: if client_id in video_queues:
if not video_queues[client_id].full(): if not video_queues[client_id].full():
video_queues[client_id].put(message) video_queues[client_id].put(message)
@@ -1074,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") print(f"[WebSocket Client] ⚠️ {client_id}: video queue is FULL, dropping frame")
else: else:
print(f"[WebSocket Client] ❌ {client_id}: video_queue not found!") 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): elif isinstance(message, str):
try: try:
@@ -1126,10 +1116,10 @@ async def client_websocket_endpoint(websocket: WebSocket, room_id: str, password
if video_processor: if video_processor:
video_processor.stop() video_processor.stop()
# Очищаем очередь админа, чтобы фреймы перестали отправляться # Очищаем очередь для администраторов
try: try:
if admin_frame_queues is not None and client_id in admin_frame_queues: if client_id in processed_video_queues:
del admin_frame_queues[client_id] del processed_video_queues[client_id]
except Exception: except Exception:
pass pass
print(f"[WebSocket Client] ✓ VideoProcessor stopped for {client_id}") print(f"[WebSocket Client] ✓ VideoProcessor stopped for {client_id}")
@@ -1138,21 +1128,21 @@ async def client_websocket_endpoint(websocket: WebSocket, room_id: str, password
@app.websocket("/ws/admin/{session_id}") @app.websocket("/ws/admin/{session_id}")
async def admin_websocket_endpoint(websocket: WebSocket, session_id: str): async def admin_websocket_endpoint(websocket: WebSocket, session_id: str):
"""WebSocket для администраторов (просмотр и управление)""" """WebSocket для администраторов (просмотр и управление)"""
global admin_sessions
if not AdminAuth.verify_session(session_id): 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) await websocket.close(code=1008)
return return
await websocket.accept() await websocket.accept()
admin_websockets[session_id] = websocket admin_websockets[session_id] = websocket
# Словарь для хранения активных стримов
active_streams = {}
try: try:
await websocket.send_text(json.dumps({ await websocket.send_text(json.dumps({
"type": "connected", "type": "connected",
"message": "Admin WebSocket connected" "message": "Admin WebSocket connected",
"session_id": session_id
})) }))
print(f"[WebSocket] Admin connected: {session_id}") print(f"[WebSocket] Admin connected: {session_id}")
@@ -1171,7 +1161,33 @@ async def admin_websocket_endpoint(websocket: WebSocket, session_id: str):
if cmd_type == "watch_client": if cmd_type == "watch_client":
client_id = command.get("client_id") 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": elif cmd_type == "control_client":
client_id = command.get("client_id") client_id = command.get("client_id")
@@ -1181,7 +1197,8 @@ async def admin_websocket_endpoint(websocket: WebSocket, session_id: str):
command_queues[client_id].put(control_cmd) command_queues[client_id].put(control_cmd)
await websocket.send_text(json.dumps({ await websocket.send_text(json.dumps({
"type": "control_response", "type": "control_response",
"success": True "success": True,
"command": control_cmd
})) }))
elif cmd_type == "get_stats": elif cmd_type == "get_stats":
@@ -1209,67 +1226,99 @@ async def admin_websocket_endpoint(websocket: WebSocket, session_id: str):
except Exception as e: except Exception as e:
print(f"[WebSocket] Admin connection error: {e}") print(f"[WebSocket] Admin connection error: {e}")
finally: finally:
# Останавливаем все активные стримы
if session_id in active_streams:
active_streams[session_id].cancel()
if session_id in admin_websockets: if session_id in admin_websockets:
del admin_websockets[session_id] del admin_websockets[session_id]
async def _stream_client_to_admin(client_id: str, admin_session_id: str): 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: if client_id not in clients or admin_session_id not in admin_websockets:
return return
try: try:
ws = admin_websockets[admin_session_id] admin_ws = admin_websockets[admin_session_id]
# Уведомляем админа о старте стрима
await ws.send_text(json.dumps({"type": "stream_started", "client_id": client_id})) await admin_ws.send_text(json.dumps({
"type": "stream_started",
"client_id": client_id,
"timestamp": datetime.now().isoformat()
}))
# Получаем информацию о клиенте
with stats_lock: with stats_lock:
if client_id in clients: if client_id in clients:
client = clients[client_id] client = clients[client_id]
await ws.send_text(json.dumps({ await admin_ws.send_text(json.dumps({
"type": "stream_info", "type": "stream_info",
"message": f"Streaming from client {client_id}", "message": f"Streaming from client {client_id}",
"quality": client.get("video_settings", {}).get("quality"), "client_id": client_id,
"ip": client.get("ip_address"), "quality": client["video_settings"]["quality"],
"connected_at": client.get("connected_at") "frame_rate": client["video_settings"]["frame_rate"],
"resolution": client["video_settings"]["resolution"],
"ip": client["ip_address"],
"connected_at": client["connected_at"],
"status": "streaming"
})) }))
# Подготовим очередь для пересылки байтов администраторам print(f"[WebSocket] Started streaming from client {client_id} to admin {admin_session_id}")
if admin_frame_queues is None:
# Ждем пока появится очередь обработанных кадров
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 return
if client_id not in admin_frame_queues: processed_queue = processed_video_queues[client_id]
# Создаём очередь для этого клиента, если её нет frame_count = 0
admin_frame_queues[client_id] = asyncio.Queue(maxsize=128) last_update_time = time.time()
aq = admin_frame_queues[client_id] # Основной цикл пересылки видео
print(f"[WebSocket] Started streaming frames to admin {admin_session_id} for client {client_id}") while (client_id in clients and
admin_session_id in admin_websockets and
# Цикл чтения кадров из очереди и отправки их админу clients[client_id].get("is_streaming", False)):
# Выходим только если очередь удалена (клиент отключился) или произойдёт исключение
try: try:
while client_id in admin_frame_queues: # Получаем обработанный кадр
try: if not processed_queue.empty():
# Пытаемся получить кадр с таймаутом, чтобы периодически проверять наличие очереди frame_data = processed_queue.get_nowait()
frame_bytes = await asyncio.wait_for(aq.get(), timeout=2.0) frame_count += 1
try:
await ws.send_bytes(frame_bytes) # Отправляем кадр администратору как бинарные данные
except Exception as e: await admin_ws.send_bytes(frame_data)
print(f"[WebSocket] Error sending bytes to admin {admin_session_id}: {e}")
break # Отправляем статистику каждую секунду
except asyncio.TimeoutError: current_time = time.time()
# Таймаут OK — просто продолжаем ждать if current_time - last_update_time > 1:
continue await admin_ws.send_text(json.dumps({
except asyncio.CancelledError: "type": "stream_stats",
break "frames_sent": frame_count,
except Exception as e: "timestamp": datetime.now().isoformat()
print(f"[WebSocket] Error in admin frame loop: {e}") }))
finally: last_update_time = current_time
print(f"[WebSocket] Stopped streaming frames to admin {admin_session_id} for client {client_id}")
# Небольшая задержка для 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: except Exception as e:
print(f"[WebSocket] Error streaming to admin: {e}") print(f"[WebSocket] Error in stream to admin: {e}")
# ========== КЛИЕНТСКИЙ API ========== # ========== КЛИЕНТСКИЙ API ==========
@app.post("/api/client/connect") @app.post("/api/client/connect")
@@ -1283,11 +1332,12 @@ async def client_connect_api(connection_data: dict):
if client_id: if client_id:
server_host = get_server_host() server_host = get_server_host()
ws_protocol = "wss" if SERVER_CONFIG.get("ssl_enabled", False) else "ws"
return { return {
"success": True, "success": True,
"client_id": client_id, "client_id": client_id,
"room_id": room_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"} return {"success": False, "error": "Connection failed"}
@@ -2492,7 +2542,7 @@ def create_html_templates():
<div class="connection-info"> <div class="connection-info">
<h4>Client Connection Information</h4> <h4>Client Connection Information</h4>
<p>Clients can connect to this room using:</p> <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>Room ID:</strong> <code>{{ room.id }}</code></p>
<p><strong>Password:</strong> <code>{{ room.password }}</code></p> <p><strong>Password:</strong> <code>{{ room.password }}</code></p>
</div> </div>
@@ -3038,8 +3088,9 @@ def create_html_templates():
// Show room preview // Show room preview
previewDiv.style.display = 'block'; previewDiv.style.display = 'block';
document.getElementById('previewId').textContent = result.room.id; document.getElementById('previewId').textContent = result.room.id;
const wsProto = window.location.protocol === 'https:' ? 'wss' : 'ws';
document.getElementById('previewWsUrl').textContent = 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 // Clear form
this.reset(); this.reset();
@@ -3428,6 +3479,9 @@ def create_html_templates():
<div class="main-content"> <div class="main-content">
<!-- Video Container --> <!-- Video Container -->
<div class="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"> <div class="video-placeholder" id="videoPlaceholder">
<i class="fas fa-video"></i> <i class="fas fa-video"></i>
<h3>Waiting for Video Stream</h3> <h3>Waiting for Video Stream</h3>
@@ -3559,7 +3613,8 @@ def create_html_templates():
return; 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 { try {
ws = new WebSocket(wsUrl); ws = new WebSocket(wsUrl);
@@ -3575,10 +3630,17 @@ def create_html_templates():
ws.onmessage = function(event) { ws.onmessage = function(event) {
try { try {
const data = JSON.parse(event.data); // Проверяем, это бинарные данные (видеокадр) или текст (JSON)
handleWebSocketMessage(data); 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) { } catch (error) {
console.error('Error parsing WebSocket message:', error); console.error('Error processing WebSocket message:', error);
} }
}; };
@@ -3737,6 +3799,39 @@ def create_html_templates():
} }
} }
// 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 // Initialize sliders
function initializeSliders() { function initializeSliders() {
// Quality slider // Quality slider
@@ -3834,31 +3929,55 @@ def main():
create_html_templates() create_html_templates()
server_host = get_server_host() 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("=" * 60)
print("🎥 Video Streaming Server with Web Interface") print("🎥 Video Streaming Server with Web Interface")
print("=" * 60) print("=" * 60)
print(f"🌐 Web Interface: http://{server_host}:{SERVER_CONFIG['port']}") print(f"🌐 Web Interface: {protocol}://{server_host}:{SERVER_CONFIG['port']}")
print(f"🔌 WebSocket: ws://{server_host}:{SERVER_CONFIG['port']}") print(f"🔌 WebSocket: {ws_protocol}://{server_host}:{SERVER_CONFIG['port']}")
print(f"👤 Admin Login: http://{server_host}:{SERVER_CONFIG['port']}/") print(f"👤 Admin Login: {protocol}://{server_host}:{SERVER_CONFIG['port']}/")
print("=" * 60) print("=" * 60)
print("Default Admin Accounts:") print("Default Admin Accounts:")
print(" • admin / admin123") print(" • admin / admin123")
print(" • administrator / securepass") print(" • administrator / securepass")
print(" • supervisor / superpass") print(" • supervisor / superpass")
print("=" * 60) 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") print("Press Ctrl+C to stop the server")
try: try:
uvicorn.run( uvicorn_kwargs = {
"server:app", "host": SERVER_CONFIG["host"],
host=SERVER_CONFIG["host"], "port": SERVER_CONFIG["port"],
port=SERVER_CONFIG["port"], "log_level": "info",
reload=False, "ws_ping_interval": SERVER_CONFIG["websocket_ping_interval"],
log_level="info", "ws_ping_timeout": SERVER_CONFIG["websocket_ping_timeout"],
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: except KeyboardInterrupt:
print("\nServer stopped by user") print("\nServer stopped by user")
except Exception as e: 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 // Show room preview
previewDiv.style.display = 'block'; previewDiv.style.display = 'block';
document.getElementById('previewId').textContent = result.room.id; document.getElementById('previewId').textContent = result.room.id;
const wsProto = window.location.protocol === 'https:' ? 'wss' : 'ws';
document.getElementById('previewWsUrl').textContent = 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 // Clear form
this.reset(); this.reset();

View File

@@ -422,7 +422,7 @@
<div class="connection-info"> <div class="connection-info">
<h4>Client Connection Information</h4> <h4>Client Connection Information</h4>
<p>Clients can connect to this room using:</p> <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>Room ID:</strong> <code>{{ room.id }}</code></p>
<p><strong>Password:</strong> <code>{{ room.password }}</code></p> <p><strong>Password:</strong> <code>{{ room.password }}</code></p>
</div> </div>

View File

@@ -350,6 +350,9 @@
<div class="main-content"> <div class="main-content">
<!-- Video Container --> <!-- Video Container -->
<div class="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"> <div class="video-placeholder" id="videoPlaceholder">
<i class="fas fa-video"></i> <i class="fas fa-video"></i>
<h3>Waiting for Video Stream</h3> <h3>Waiting for Video Stream</h3>
@@ -481,7 +484,8 @@
return; 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 { try {
ws = new WebSocket(wsUrl); ws = new WebSocket(wsUrl);
@@ -497,10 +501,17 @@
ws.onmessage = function(event) { ws.onmessage = function(event) {
try { try {
const data = JSON.parse(event.data); // Проверяем, это бинарные данные (видеокадр) или текст (JSON)
handleWebSocketMessage(data); 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) { } 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 // Initialize sliders
function initializeSliders() { function initializeSliders() {
// Quality slider // Quality slider

117
test_admin_watch.py Normal file
View File

@@ -0,0 +1,117 @@
#!/usr/bin/env python3
"""
Скрипт для подключения админа и просмотра видео от клиента
Требует, чтобы есть активное подключение клиента, отправляющего кадры
"""
import asyncio
import websockets
import json
import requests
from dotenv import load_dotenv
import os
# Загружаем переменные окружения из .env
load_dotenv()
# Параметры подключения
HOST = os.getenv("PUBLIC_HOST", "192.168.0.112")
PORT = int(os.getenv("PORT", 8000))
# Учетные данные админа (из server.py)
ADMIN_USERNAME = "admin"
ADMIN_PASSWORD = "admin123"
async def admin_watch():
"""Админ логинится и смотрит видео от клиента"""
# 1. Логинимся как админ
print(f"[Admin] Logging in as {ADMIN_USERNAME}...")
session = requests.Session()
try:
login_response = session.post(
f"http://{HOST}:{PORT}/login",
data={
"username": ADMIN_USERNAME,
"password": ADMIN_PASSWORD
}
)
if login_response.status_code != 303 and login_response.status_code != 200:
print(f"[Admin] ❌ Failed to login: {login_response.status_code}")
return
# Получаем session_id из cookies
session_id = session.cookies.get("session_id")
if not session_id:
print(f"[Admin] ❌ No session_id in cookies")
print(f"[Admin] Cookies: {session.cookies}")
return
print(f"[Admin] ✓ Logged in, session_id: {session_id[:16]}...")
except Exception as e:
print(f"[Admin] ❌ Login error: {e}")
return
# 2. Получаем список активных комнат и клиентов
print(f"[Admin] Getting active clients...")
try:
dashboard_response = session.get(f"http://{HOST}:{PORT}/dashboard")
if dashboard_response.status_code != 200:
print(f"[Admin] ❌ Failed to get dashboard: {dashboard_response.status_code}")
return
# Парсим HTML чтобы найти client_id (это сложновато, используем API)
# Для простоты - просто выведем сообщение о том, как найти client_id
print(f"[Admin] 💡 Open http://{HOST}:{PORT}/dashboard in browser to see active clients")
print(f"[Admin] 💡 Look for client_id in the page source or network tab")
except Exception as e:
print(f"[Admin] ❌ Error: {e}")
return
# 3. Для тестирования подключимся к вебсокету админа
uri = f"ws://{HOST}:{PORT}/ws/admin/{session_id}"
print(f"[Admin] Connecting to WebSocket: {uri}")
try:
async with websockets.connect(uri) as ws:
# Получаем ответ сервера
response = await asyncio.wait_for(ws.recv(), timeout=5)
print(f"[Admin] ✓ Server response: {response}")
# Пример: отправляем команду "watch_client"
# Вам нужно знать client_id активного клиента
print(f"[Admin] 💡 To watch a client, send: {{'type': 'watch_client', 'client_id': 'CLIENT_ID_HERE'}}")
print(f"[Admin] 💡 After that, admin will receive binary video frames")
# Ждём сообщений от сервера
print(f"[Admin] Waiting for messages from server (press Ctrl+C to stop)...")
try:
while True:
try:
message = await asyncio.wait_for(ws.recv(), timeout=5)
if isinstance(message, bytes):
print(f"[Admin] ✓ Received {len(message)} bytes of video frame data")
else:
print(f"[Admin] ✓ Message: {message[:100]}")
except asyncio.TimeoutError:
# Таймаут OK - просто ждём дальше
continue
except KeyboardInterrupt:
print(f"\n[Admin] Disconnected")
except Exception as e:
print(f"[Admin] ❌ WebSocket error: {e}")
if __name__ == "__main__":
print("=" * 60)
print("Admin WebSocket Test")
print("=" * 60)
print(f"Server: http://{HOST}:{PORT}")
print()
try:
asyncio.run(admin_watch())
except KeyboardInterrupt:
print("\n[Admin] Interrupted by user")