Compare commits
4 Commits
a6b3c54534
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| db1c4958ea | |||
| c21bbb779f | |||
| a30239a4fe | |||
| 52abd9d1f7 |
385
server.py
385
server.py
@@ -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
20
ssl/cert.pem
Normal 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
28
ssl/key.pem
Normal 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-----
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
117
test_admin_watch.py
Normal 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")
|
||||||
Reference in New Issue
Block a user