init commit
This commit is contained in:
12
.dockerignore
Normal file
12
.dockerignore
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
.pytest_cache/
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
env/
|
||||||
|
.env
|
||||||
|
.git/
|
||||||
|
node_modules/
|
||||||
|
*.log
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
env/
|
||||||
|
.env
|
||||||
|
.git/
|
||||||
|
__pycache__/
|
||||||
|
.history/
|
||||||
31
Dockerfile
Normal file
31
Dockerfile
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
|
PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Устанавливаем системные зависимости, которые часто требуются для opencv и ffmpeg
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends \
|
||||||
|
build-essential \
|
||||||
|
libglib2.0-0 \
|
||||||
|
libsm6 \
|
||||||
|
libxrender1 \
|
||||||
|
libxext6 \
|
||||||
|
ffmpeg \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Копируем зависимости и устанавливаем Python-пакеты
|
||||||
|
COPY req.txt /app/req.txt
|
||||||
|
RUN python -m pip install --upgrade pip setuptools wheel \
|
||||||
|
&& pip install --no-cache-dir -r /app/req.txt
|
||||||
|
|
||||||
|
# Копируем проект
|
||||||
|
COPY . /app
|
||||||
|
|
||||||
|
# Открытый порт
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
# По умолчанию запускаем uvicorn
|
||||||
|
CMD ["uvicorn", "server:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
28
README_DOCKER.md
Normal file
28
README_DOCKER.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# Сборка и запуск в Docker
|
||||||
|
|
||||||
|
Краткие инструкции по сборке и запуску контейнера локально.
|
||||||
|
|
||||||
|
Собрать образ:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t camera_server:latest .
|
||||||
|
```
|
||||||
|
|
||||||
|
Запустить контейнер:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm -p 8000:8000 \
|
||||||
|
-v "$(pwd)/static:/app/static" \
|
||||||
|
-v "$(pwd)/templates:/app/templates" \
|
||||||
|
--name camera_server camera_server:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Или с помощью docker-compose:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose up --build
|
||||||
|
```
|
||||||
|
|
||||||
|
Примечания:
|
||||||
|
- Если у вас headless-сервер и вы не используете GUI-возможности OpenCV, рассмотрите замену `opencv-python` на `opencv-python-headless` в `req.txt`.
|
||||||
|
- При проблемах со сборкой на некоторых платформах установите необходимые системные пакеты (в Dockerfile уже перечислены распространённые зависимости).
|
||||||
12
docker-compose.yml
Normal file
12
docker-compose.yml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
version: '3.8'
|
||||||
|
services:
|
||||||
|
camera_server:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- ./static:/app/static
|
||||||
|
- ./templates:/app/templates
|
||||||
|
environment:
|
||||||
|
- PYTHONUNBUFFERED=1
|
||||||
11
req.txt
Normal file
11
req.txt
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
fastapi
|
||||||
|
uvicorn
|
||||||
|
opencv-python
|
||||||
|
psutil
|
||||||
|
numpy
|
||||||
|
jinja2
|
||||||
|
uvicorn[standard]
|
||||||
|
typing-extensions
|
||||||
|
aiofiles
|
||||||
|
websockets
|
||||||
|
python-multipart
|
||||||
450
templates/create_room.html
Normal file
450
templates/create_room.html
Normal file
@@ -0,0 +1,450 @@
|
|||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Create Room - Video Streaming Server</title>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--primary: #667eea;
|
||||||
|
--secondary: #764ba2;
|
||||||
|
--success: #10b981;
|
||||||
|
--danger: #ef4444;
|
||||||
|
--warning: #f59e0b;
|
||||||
|
--info: #3b82f6;
|
||||||
|
--dark: #1f2937;
|
||||||
|
--light: #f9fafb;
|
||||||
|
--gray: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: var(--dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar */
|
||||||
|
.sidebar {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100vh;
|
||||||
|
width: 250px;
|
||||||
|
background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 20px 0;
|
||||||
|
box-shadow: 0 0 20px rgba(0,0,0,0.1);
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
padding: 0 20px 30px;
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo p {
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links li {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links a {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 15px;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
transition: background 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links a:hover {
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links a.active {
|
||||||
|
background: rgba(255,255,255,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links i {
|
||||||
|
margin-right: 10px;
|
||||||
|
width: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 15px;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
transition: background 0.3s;
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
margin: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn:hover {
|
||||||
|
background: rgba(255,255,255,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn i {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main Content */
|
||||||
|
.main-content {
|
||||||
|
margin-left: 250px;
|
||||||
|
padding: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h2 {
|
||||||
|
font-size: 28px;
|
||||||
|
color: var(--dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form */
|
||||||
|
.form-container {
|
||||||
|
background: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 30px;
|
||||||
|
box-shadow: 0 5px 15px rgba(0,0,0,0.05);
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: var(--gray);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input,
|
||||||
|
.form-group select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 16px;
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:focus,
|
||||||
|
.form-group select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group .help-text {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--gray);
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 12px 24px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: var(--gray);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Alert */
|
||||||
|
.alert {
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-success {
|
||||||
|
background: #d1fae5;
|
||||||
|
color: var(--success);
|
||||||
|
border: 1px solid #a7f3d0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-error {
|
||||||
|
background: #fee2e2;
|
||||||
|
color: var(--danger);
|
||||||
|
border: 1px solid #fecaca;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Room Preview */
|
||||||
|
.room-preview {
|
||||||
|
background: var(--light);
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-top: 30px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.room-preview h4 {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
color: var(--dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-item {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-item label {
|
||||||
|
display: inline-block;
|
||||||
|
width: 120px;
|
||||||
|
color: var(--gray);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.sidebar {
|
||||||
|
width: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .logo h1,
|
||||||
|
.sidebar .logo p,
|
||||||
|
.sidebar .nav-links span,
|
||||||
|
.sidebar .back-btn span {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .nav-links a {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .nav-links i {
|
||||||
|
margin-right: 0;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .back-btn {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
margin-left: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-container {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<div class="sidebar">
|
||||||
|
<div class="logo">
|
||||||
|
<h1>🎥 VSS</h1>
|
||||||
|
<p>Video Streaming Server</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="nav-links">
|
||||||
|
<li>
|
||||||
|
<a href="/dashboard">
|
||||||
|
<i class="fas fa-tachometer-alt"></i>
|
||||||
|
<span>Dashboard</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/create-room" class="active">
|
||||||
|
<i class="fas fa-plus-circle"></i>
|
||||||
|
<span>Create Room</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<a href="/dashboard" class="back-btn">
|
||||||
|
<i class="fas fa-arrow-left"></i>
|
||||||
|
<span>Back to Dashboard</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<div class="main-content">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="header">
|
||||||
|
<h2>Create New Room</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Alert Messages -->
|
||||||
|
<div id="alert" class="alert"></div>
|
||||||
|
|
||||||
|
<!-- Form -->
|
||||||
|
<div class="form-container">
|
||||||
|
<form id="createRoomForm" method="POST" action="/api/create-room">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="name">Room Name *</label>
|
||||||
|
<input type="text" id="name" name="name" required
|
||||||
|
placeholder="e.g., Conference Room, Surveillance Camera">
|
||||||
|
<div class="help-text">A descriptive name for the room</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">Room Password *</label>
|
||||||
|
<input type="text" id="password" name="password" required
|
||||||
|
placeholder="Enter a secure password">
|
||||||
|
<div class="help-text">Clients will need this password to connect</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="max_connections">Max Connections *</label>
|
||||||
|
<select id="max_connections" name="max_connections" required>
|
||||||
|
<option value="1">1 client</option>
|
||||||
|
<option value="5" selected>5 clients</option>
|
||||||
|
<option value="10">10 clients</option>
|
||||||
|
<option value="20">20 clients</option>
|
||||||
|
<option value="50">50 clients</option>
|
||||||
|
</select>
|
||||||
|
<div class="help-text">Maximum number of clients that can connect simultaneously</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="fas fa-plus-circle"></i> Create Room
|
||||||
|
</button>
|
||||||
|
<a href="/dashboard" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-times"></i> Cancel
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Room Preview -->
|
||||||
|
<div id="roomPreview" class="room-preview">
|
||||||
|
<h4>Room Information</h4>
|
||||||
|
<div class="preview-item">
|
||||||
|
<label>Room ID:</label>
|
||||||
|
<span id="previewId">Generating...</span>
|
||||||
|
</div>
|
||||||
|
<div class="preview-item">
|
||||||
|
<label>WebSocket URL:</label>
|
||||||
|
<code id="previewWsUrl">ws://{{ server_host }}:{{ server_port }}/ws/client/...</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.getElementById('createRoomForm').addEventListener('submit', async function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const formData = new FormData(this);
|
||||||
|
const alertDiv = document.getElementById('alert');
|
||||||
|
const previewDiv = document.getElementById('roomPreview');
|
||||||
|
|
||||||
|
// Show loading
|
||||||
|
alertDiv.className = 'alert alert-success';
|
||||||
|
alertDiv.style.display = 'block';
|
||||||
|
alertDiv.textContent = 'Creating room...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/create-room', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
alertDiv.className = 'alert alert-success';
|
||||||
|
alertDiv.textContent = 'Room created successfully!';
|
||||||
|
|
||||||
|
// Show room preview
|
||||||
|
previewDiv.style.display = 'block';
|
||||||
|
document.getElementById('previewId').textContent = result.room.id;
|
||||||
|
document.getElementById('previewWsUrl').textContent =
|
||||||
|
`ws://${window.location.hostname}:{{ server_port }}/ws/client/${result.room.id}/${result.room.password}`;
|
||||||
|
|
||||||
|
// Clear form
|
||||||
|
this.reset();
|
||||||
|
|
||||||
|
// Redirect to room page after 3 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.href = `/room/${result.room.id}`;
|
||||||
|
}, 3000);
|
||||||
|
} else {
|
||||||
|
alertDiv.className = 'alert alert-error';
|
||||||
|
alertDiv.textContent = 'Error: ' + (result.error || 'Failed to create room');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alertDiv.className = 'alert alert-error';
|
||||||
|
alertDiv.textContent = 'Error: ' + error.message;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Generate a random password suggestion
|
||||||
|
function generatePassword() {
|
||||||
|
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||||
|
let password = '';
|
||||||
|
for (let i = 0; i < 8; i++) {
|
||||||
|
password += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||||
|
}
|
||||||
|
document.getElementById('password').value = password;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-generate password on page load
|
||||||
|
window.addEventListener('load', generatePassword);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
595
templates/dashboard.html
Normal file
595
templates/dashboard.html
Normal file
@@ -0,0 +1,595 @@
|
|||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Dashboard - Video Streaming Server</title>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--primary: #667eea;
|
||||||
|
--secondary: #764ba2;
|
||||||
|
--success: #10b981;
|
||||||
|
--danger: #ef4444;
|
||||||
|
--warning: #f59e0b;
|
||||||
|
--info: #3b82f6;
|
||||||
|
--dark: #1f2937;
|
||||||
|
--light: #f9fafb;
|
||||||
|
--gray: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: var(--dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar */
|
||||||
|
.sidebar {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100vh;
|
||||||
|
width: 250px;
|
||||||
|
background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 20px 0;
|
||||||
|
box-shadow: 0 0 20px rgba(0,0,0,0.1);
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
padding: 0 20px 30px;
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo p {
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links li {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links a {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 15px;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
transition: background 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links a:hover {
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links a.active {
|
||||||
|
background: rgba(255,255,255,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links i {
|
||||||
|
margin-right: 10px;
|
||||||
|
width: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 20px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: 20px;
|
||||||
|
border-top: 1px solid rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info p {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-btn {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px;
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: background 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-btn:hover {
|
||||||
|
background: rgba(255,255,255,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main Content */
|
||||||
|
.main-content {
|
||||||
|
margin-left: 250px;
|
||||||
|
padding: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h2 {
|
||||||
|
font-size: 28px;
|
||||||
|
color: var(--dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success {
|
||||||
|
background: var(--success);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: var(--danger);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stats Cards */
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
background: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 5px 15px rgba(0,0,0,0.05);
|
||||||
|
border-left: 4px solid var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card h3 {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--gray);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card .value {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--dark);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card .change {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--success);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Rooms Table */
|
||||||
|
.section {
|
||||||
|
background: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 25px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
box-shadow: 0 5px 15px rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 20px;
|
||||||
|
color: var(--dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
thead {
|
||||||
|
background: var(--light);
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
padding: 15px;
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--gray);
|
||||||
|
border-bottom: 2px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
padding: 15px;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:hover {
|
||||||
|
background: var(--light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-active {
|
||||||
|
background: #d1fae5;
|
||||||
|
color: var(--success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-inactive {
|
||||||
|
background: #fee2e2;
|
||||||
|
color: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm {
|
||||||
|
padding: 5px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon {
|
||||||
|
padding: 8px;
|
||||||
|
width: 35px;
|
||||||
|
height: 35px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading */
|
||||||
|
.loading {
|
||||||
|
text-align: center;
|
||||||
|
padding: 50px;
|
||||||
|
color: var(--gray);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.sidebar {
|
||||||
|
width: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .logo h1,
|
||||||
|
.sidebar .logo p,
|
||||||
|
.sidebar .nav-links span,
|
||||||
|
.sidebar .user-info p {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .nav-links a {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .nav-links i {
|
||||||
|
margin-right: 0;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
margin-left: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<div class="sidebar">
|
||||||
|
<div class="logo">
|
||||||
|
<h1>🎥 VSS</h1>
|
||||||
|
<p>Video Streaming Server</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="nav-links">
|
||||||
|
<li>
|
||||||
|
<a href="/dashboard" class="active">
|
||||||
|
<i class="fas fa-tachometer-alt"></i>
|
||||||
|
<span>Dashboard</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/create-room">
|
||||||
|
<i class="fas fa-plus-circle"></i>
|
||||||
|
<span>Create Room</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#">
|
||||||
|
<i class="fas fa-cog"></i>
|
||||||
|
<span>Settings</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="user-info">
|
||||||
|
<p>Logged in as: <strong>{{ username }}</strong></p>
|
||||||
|
<a href="/logout">
|
||||||
|
<button class="logout-btn">
|
||||||
|
<i class="fas fa-sign-out-alt"></i> Logout
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<div class="main-content">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="header">
|
||||||
|
<h2>Dashboard</h2>
|
||||||
|
<div class="header-actions">
|
||||||
|
<button class="btn btn-success" onclick="refreshStats()">
|
||||||
|
<i class="fas fa-sync-alt"></i> Refresh
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Stats Cards -->
|
||||||
|
<div class="stats-grid" id="statsGrid">
|
||||||
|
<div class="stat-card">
|
||||||
|
<h3>Total Rooms</h3>
|
||||||
|
<div class="value">{{ stats.total_rooms }}</div>
|
||||||
|
<div class="change">Active: {{ total_rooms }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-card">
|
||||||
|
<h3>Connected Clients</h3>
|
||||||
|
<div class="value">{{ stats.total_clients }}</div>
|
||||||
|
<div class="change">Streaming: {{ stats.total_streams }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-card">
|
||||||
|
<h3>CPU Usage</h3>
|
||||||
|
<div class="value">{{ stats.cpu_usage }}%</div>
|
||||||
|
<div class="change">Cores: {{ stats.system.cpu_count if stats.system else 'N/A' }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-card">
|
||||||
|
<h3>Memory Usage</h3>
|
||||||
|
<div class="value">{{ stats.memory_usage }}%</div>
|
||||||
|
<div class="change">
|
||||||
|
{% if stats.system %}
|
||||||
|
{{ ((stats.system.memory_total - stats.system.memory_available) / 1024 / 1024 / 1024)|round(1) }} GB used
|
||||||
|
{% else %}
|
||||||
|
0 GB used
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Rooms Section -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h3 class="section-title">Rooms</h3>
|
||||||
|
<a href="/create-room">
|
||||||
|
<button class="btn btn-primary">
|
||||||
|
<i class="fas fa-plus"></i> Create Room
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if rooms %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Clients</th>
|
||||||
|
<th>Max Connections</th>
|
||||||
|
<th>Created By</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for room in rooms %}
|
||||||
|
<tr>
|
||||||
|
<td><code>{{ room.id }}</code></td>
|
||||||
|
<td>{{ room.name }}</td>
|
||||||
|
<td>
|
||||||
|
<span class="value">{{ room.clients_count }}</span>
|
||||||
|
{% if room.active_streams > 0 %}
|
||||||
|
<span class="change">({{ room.active_streams }} streaming)</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{{ room.max_connections }}</td>
|
||||||
|
<td>{{ room.created_by }}</td>
|
||||||
|
<td>
|
||||||
|
<span class="status-badge status-active">Active</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="action-buttons">
|
||||||
|
<a href="/room/{{ room.id }}">
|
||||||
|
<button class="btn btn-primary btn-sm">
|
||||||
|
<i class="fas fa-eye"></i> View
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
|
<button class="btn btn-danger btn-sm" onclick="deleteRoom('{{ room.id }}')">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="loading">
|
||||||
|
<p>No rooms created yet. Create your first room!</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- System Info -->
|
||||||
|
<div class="section">
|
||||||
|
<h3 class="section-title">System Information</h3>
|
||||||
|
<div class="stats-grid">
|
||||||
|
<div class="stat-card">
|
||||||
|
<h3>Server Uptime</h3>
|
||||||
|
<div class="value" id="uptime">Loading...</div>
|
||||||
|
<div class="change">Since {{ stats.start_time }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-card">
|
||||||
|
<h3>Server Address</h3>
|
||||||
|
<div class="value">{{ server_host }}:{{ server_port }}</div>
|
||||||
|
<div class="change">WebSocket: ws://{{ server_host }}:{{ server_port }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-card">
|
||||||
|
<h3>API Status</h3>
|
||||||
|
<div class="value">Online</div>
|
||||||
|
<div class="change change-up">All systems operational</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function formatUptime(seconds) {
|
||||||
|
const days = Math.floor(seconds / 86400);
|
||||||
|
const hours = Math.floor((seconds % 86400) / 3600);
|
||||||
|
const minutes = Math.floor((seconds % 3600) / 60);
|
||||||
|
|
||||||
|
let result = [];
|
||||||
|
if (days > 0) result.push(`${days}d`);
|
||||||
|
if (hours > 0) result.push(`${hours}h`);
|
||||||
|
if (minutes > 0) result.push(`${minutes}m`);
|
||||||
|
|
||||||
|
return result.join(' ') || 'Just started';
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateUptime() {
|
||||||
|
const uptimeElement = document.getElementById('uptime');
|
||||||
|
if (uptimeElement && {{ stats.uptime }}) {
|
||||||
|
uptimeElement.textContent = formatUptime({{ stats.uptime }});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteRoom(roomId) {
|
||||||
|
if (confirm('Are you sure you want to delete this room? All clients will be disconnected.')) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/delete-room/${roomId}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
alert('Room deleted successfully!');
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert('Error: ' + (result.error || 'Failed to delete room'));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert('Error: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshStats() {
|
||||||
|
try {
|
||||||
|
const sessionId = getCookie('session_id');
|
||||||
|
if (!sessionId) {
|
||||||
|
window.location.href = '/';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`/api/stats?session_id=${sessionId}`);
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error refreshing stats:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCookie(name) {
|
||||||
|
const value = `; ${document.cookie}`;
|
||||||
|
const parts = value.split(`; ${name}=`);
|
||||||
|
if (parts.length === 2) return parts.pop().split(';').shift();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update uptime every minute
|
||||||
|
updateUptime();
|
||||||
|
setInterval(updateUptime, 60000);
|
||||||
|
|
||||||
|
// Auto-refresh every 30 seconds
|
||||||
|
setInterval(refreshStats, 30000);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
172
templates/login.html
Normal file
172
templates/login.html
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Video Streaming Server - Login</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-container {
|
||||||
|
background: white;
|
||||||
|
padding: 40px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo h1 {
|
||||||
|
color: #333;
|
||||||
|
font-size: 28px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo p {
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
color: #555;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 16px;
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
background: #ffebee;
|
||||||
|
color: #c62828;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-accounts {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-accounts h3 {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-accounts ul {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-accounts li {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
color: #666;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="login-container">
|
||||||
|
<div class="logo">
|
||||||
|
<h1>🎥 Video Streaming Server</h1>
|
||||||
|
<p>Admin Panel</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if error %}
|
||||||
|
<div class="error">
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form method="POST" action="/login">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="username">Username</label>
|
||||||
|
<input type="text" id="username" name="username" required placeholder="Enter username">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<input type="password" id="password" name="password" required placeholder="Enter password">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn">Login</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="demo-accounts">
|
||||||
|
<h3>Demo Accounts:</h3>
|
||||||
|
<ul>
|
||||||
|
<li><strong>admin</strong> / admin123</li>
|
||||||
|
<li><strong>administrator</strong> / securepass</li>
|
||||||
|
<li><strong>supervisor</strong> / superpass</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
Version 2.1.0 • Server: {{ server_host }}:{{ server_port }}
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
554
templates/room.html
Normal file
554
templates/room.html
Normal file
@@ -0,0 +1,554 @@
|
|||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Room: {{ room.name }} - Video Streaming Server</title>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--primary: #667eea;
|
||||||
|
--secondary: #764ba2;
|
||||||
|
--success: #10b981;
|
||||||
|
--danger: #ef4444;
|
||||||
|
--warning: #f59e0b;
|
||||||
|
--info: #3b82f6;
|
||||||
|
--dark: #1f2937;
|
||||||
|
--light: #f9fafb;
|
||||||
|
--gray: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: var(--dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar */
|
||||||
|
.sidebar {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100vh;
|
||||||
|
width: 250px;
|
||||||
|
background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 20px 0;
|
||||||
|
box-shadow: 0 0 20px rgba(0,0,0,0.1);
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
padding: 0 20px 30px;
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo p {
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links li {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links a {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 15px;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
transition: background 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links a:hover {
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links a.active {
|
||||||
|
background: rgba(255,255,255,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links i {
|
||||||
|
margin-right: 10px;
|
||||||
|
width: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 15px;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
transition: background 0.3s;
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
margin: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn:hover {
|
||||||
|
background: rgba(255,255,255,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn i {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main Content */
|
||||||
|
.main-content {
|
||||||
|
margin-left: 250px;
|
||||||
|
padding: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h2 {
|
||||||
|
font-size: 28px;
|
||||||
|
color: var(--dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.room-info {
|
||||||
|
background: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 25px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
box-shadow: 0 5px 15px rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.room-info-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
padding: 15px;
|
||||||
|
background: var(--light);
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item label {
|
||||||
|
display: block;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--gray);
|
||||||
|
margin-bottom: 5px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item .value {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Clients Table */
|
||||||
|
.clients-section {
|
||||||
|
background: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 25px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
box-shadow: 0 5px 15px rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 20px;
|
||||||
|
color: var(--dark);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
thead {
|
||||||
|
background: var(--light);
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
padding: 15px;
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--gray);
|
||||||
|
border-bottom: 2px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
padding: 15px;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:hover {
|
||||||
|
background: var(--light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-streaming {
|
||||||
|
background: #d1fae5;
|
||||||
|
color: var(--success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-idle {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: var(--warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: var(--danger);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success {
|
||||||
|
background: var(--success);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm {
|
||||||
|
padding: 5px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty State */
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 50px;
|
||||||
|
color: var(--gray);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Connection Info */
|
||||||
|
.connection-info {
|
||||||
|
background: #f0f9ff;
|
||||||
|
border: 1px solid #bae6fd;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 15px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-info h4 {
|
||||||
|
color: #0369a1;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-info code {
|
||||||
|
background: #e0f2fe;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.sidebar {
|
||||||
|
width: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .logo h1,
|
||||||
|
.sidebar .logo p,
|
||||||
|
.sidebar .nav-links span,
|
||||||
|
.sidebar .back-btn span {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .nav-links a {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .nav-links i {
|
||||||
|
margin-right: 0;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .back-btn {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
margin-left: 70px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<div class="sidebar">
|
||||||
|
<div class="logo">
|
||||||
|
<h1>🎥 VSS</h1>
|
||||||
|
<p>Video Streaming Server</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="nav-links">
|
||||||
|
<li>
|
||||||
|
<a href="/dashboard">
|
||||||
|
<i class="fas fa-tachometer-alt"></i>
|
||||||
|
<span>Dashboard</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/create-room">
|
||||||
|
<i class="fas fa-plus-circle"></i>
|
||||||
|
<span>Create Room</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<a href="/dashboard" class="back-btn">
|
||||||
|
<i class="fas fa-arrow-left"></i>
|
||||||
|
<span>Back to Dashboard</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<div class="main-content">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="header">
|
||||||
|
<h2>Room: {{ room.name }}</h2>
|
||||||
|
<div class="action-buttons">
|
||||||
|
<button class="btn btn-danger" onclick="deleteRoom('{{ room.id }}')">
|
||||||
|
<i class="fas fa-trash"></i> Delete Room
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Room Information -->
|
||||||
|
<div class="room-info">
|
||||||
|
<h3 class="section-title">Room Information</h3>
|
||||||
|
<div class="room-info-grid">
|
||||||
|
<div class="info-item">
|
||||||
|
<label>Room ID</label>
|
||||||
|
<div class="value"><code>{{ room.id }}</code></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-item">
|
||||||
|
<label>Password</label>
|
||||||
|
<div class="value">{{ room.password }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-item">
|
||||||
|
<label>Max Connections</label>
|
||||||
|
<div class="value">{{ room.max_connections }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-item">
|
||||||
|
<label>Created</label>
|
||||||
|
<div class="value">{{ room.created_at }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-item">
|
||||||
|
<label>Created By</label>
|
||||||
|
<div class="value">{{ room.created_by }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-item">
|
||||||
|
<label>Active Clients</label>
|
||||||
|
<div class="value">{{ room_stats.total_clients }} / {{ room.max_connections }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-item">
|
||||||
|
<label>Active Streams</label>
|
||||||
|
<div class="value">{{ room_stats.active_streams }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-item">
|
||||||
|
<label>Data Transferred</label>
|
||||||
|
<div class="value">{{ (room_stats.bytes_transferred / 1024 / 1024)|round(2) }} MB</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Connection Info for Clients -->
|
||||||
|
<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>Room ID:</strong> <code>{{ room.id }}</code></p>
|
||||||
|
<p><strong>Password:</strong> <code>{{ room.password }}</code></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Connected Clients -->
|
||||||
|
<div class="clients-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h3 class="section-title">Connected Clients ({{ clients|length }})</h3>
|
||||||
|
<button class="btn btn-primary" onclick="refreshClients()">
|
||||||
|
<i class="fas fa-sync-alt"></i> Refresh
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if clients %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Client ID</th>
|
||||||
|
<th>IP Address</th>
|
||||||
|
<th>Connected At</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Video Settings</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for client in clients %}
|
||||||
|
<tr>
|
||||||
|
<td><code>{{ client.id[:8] }}...</code></td>
|
||||||
|
<td>{{ client.ip_address }}</td>
|
||||||
|
<td>{{ client.connected_at }}</td>
|
||||||
|
<td>
|
||||||
|
{% if client.is_streaming %}
|
||||||
|
<span class="status-badge status-streaming">Streaming</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="status-badge status-idle">Idle</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ client.video_settings.quality }}% Quality<br>
|
||||||
|
{{ client.video_settings.frame_rate }} FPS
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="action-buttons">
|
||||||
|
<a href="/stream/{{ client.id }}">
|
||||||
|
<button class="btn btn-primary btn-sm">
|
||||||
|
<i class="fas fa-play"></i> Stream
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
|
<button class="btn btn-danger btn-sm" onclick="disconnectClient('{{ client.id }}')">
|
||||||
|
<i class="fas fa-sign-out-alt"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="empty-state">
|
||||||
|
<p>No clients connected to this room yet.</p>
|
||||||
|
<p>Share the connection information above with clients.</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
async function deleteRoom(roomId) {
|
||||||
|
if (confirm('Are you sure you want to delete this room? All clients will be disconnected.')) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/delete-room/${roomId}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
alert('Room deleted successfully!');
|
||||||
|
window.location.href = '/dashboard';
|
||||||
|
} else {
|
||||||
|
alert('Error: ' + (result.error || 'Failed to delete room'));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert('Error: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function disconnectClient(clientId) {
|
||||||
|
if (confirm('Are you sure you want to disconnect this client?')) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/disconnect-client/${clientId}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
alert('Client disconnected successfully!');
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert('Error: ' + (result.error || 'Failed to disconnect client'));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert('Error: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshClients() {
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-refresh every 10 seconds
|
||||||
|
setInterval(refreshClients, 10000);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
729
templates/stream.html
Normal file
729
templates/stream.html
Normal file
@@ -0,0 +1,729 @@
|
|||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Stream: {{ client.id }} - Video Streaming Server</title>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--primary: #667eea;
|
||||||
|
--secondary: #764ba2;
|
||||||
|
--success: #10b981;
|
||||||
|
--danger: #ef4444;
|
||||||
|
--warning: #f59e0b;
|
||||||
|
--info: #3b82f6;
|
||||||
|
--dark: #1f2937;
|
||||||
|
--light: #f9fafb;
|
||||||
|
--gray: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
background: #1a1a1a;
|
||||||
|
color: white;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header */
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 15px 30px;
|
||||||
|
background: rgba(0, 0, 0, 0.8);
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
border-radius: 5px;
|
||||||
|
transition: background 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn:hover {
|
||||||
|
background: rgba(255,255,255,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-info h2 {
|
||||||
|
font-size: 18px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-info p {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
background: rgba(255,255,255,0.2);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main Content */
|
||||||
|
.main-content {
|
||||||
|
display: flex;
|
||||||
|
height: calc(100vh - 70px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Video Container */
|
||||||
|
.video-container {
|
||||||
|
flex: 3;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #000;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-placeholder {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-placeholder i {
|
||||||
|
font-size: 100px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-placeholder h3 {
|
||||||
|
font-size: 24px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-placeholder p {
|
||||||
|
font-size: 14px;
|
||||||
|
text-align: center;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-stats {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 20px;
|
||||||
|
left: 20px;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
padding: 10px 15px;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Controls Panel */
|
||||||
|
.controls-panel {
|
||||||
|
flex: 1;
|
||||||
|
max-width: 300px;
|
||||||
|
background: rgba(30, 30, 30, 0.95);
|
||||||
|
padding: 20px;
|
||||||
|
overflow-y: auto;
|
||||||
|
border-left: 1px solid rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls-section {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls-section h3 {
|
||||||
|
font-size: 16px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
color: #ddd;
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-group {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
color: #aaa;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="range"] {
|
||||||
|
flex: 1;
|
||||||
|
height: 5px;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
border-radius: 5px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="range"]::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--primary);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-value {
|
||||||
|
min-width: 30px;
|
||||||
|
text-align: right;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-group {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-control {
|
||||||
|
padding: 10px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-control i {
|
||||||
|
font-size: 20px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-control span {
|
||||||
|
display: block;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Connection Info */
|
||||||
|
.info-box {
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item label {
|
||||||
|
display: inline-block;
|
||||||
|
width: 100px;
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status Indicator */
|
||||||
|
.status-indicator {
|
||||||
|
display: inline-block;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-connected {
|
||||||
|
background: var(--success);
|
||||||
|
box-shadow: 0 0 10px var(--success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-disconnected {
|
||||||
|
background: var(--danger);
|
||||||
|
box-shadow: 0 0 10px var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* WebSocket Status */
|
||||||
|
.ws-status {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 10px;
|
||||||
|
right: 10px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 11px;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-connected {
|
||||||
|
color: var(--success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-disconnected {
|
||||||
|
color: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading */
|
||||||
|
.loading {
|
||||||
|
text-align: center;
|
||||||
|
padding: 50px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.main-content {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls-panel {
|
||||||
|
max-width: none;
|
||||||
|
height: 300px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="header">
|
||||||
|
<div class="header-left">
|
||||||
|
<a href="/room/{{ client.room_id }}" class="back-btn">
|
||||||
|
<i class="fas fa-arrow-left"></i>
|
||||||
|
Back to Room
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="client-info">
|
||||||
|
<h2>Streaming: {{ client.id[:8] }}...</h2>
|
||||||
|
<p>
|
||||||
|
<span class="status-indicator status-connected"></span>
|
||||||
|
Connected from {{ client.ip_address }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="header-actions">
|
||||||
|
<button class="btn btn-danger" onclick="disconnectClient()">
|
||||||
|
<i class="fas fa-sign-out-alt"></i> Disconnect
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<div class="main-content">
|
||||||
|
<!-- Video Container -->
|
||||||
|
<div class="video-container">
|
||||||
|
<div class="video-placeholder" id="videoPlaceholder">
|
||||||
|
<i class="fas fa-video"></i>
|
||||||
|
<h3>Waiting for Video Stream</h3>
|
||||||
|
<p>Video stream will appear here once the client starts streaming.</p>
|
||||||
|
<p>Use the controls on the right to adjust video settings.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="video-stats" id="videoStats" style="display: none;">
|
||||||
|
<div>Resolution: <span id="statsResolution">640x480</span></div>
|
||||||
|
<div>Frame Rate: <span id="statsFps">30</span> FPS</div>
|
||||||
|
<div>Quality: <span id="statsQuality">85</span>%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Controls Panel -->
|
||||||
|
<div class="controls-panel">
|
||||||
|
<!-- Connection Info -->
|
||||||
|
<div class="info-box">
|
||||||
|
<div class="info-item">
|
||||||
|
<label>Client ID:</label>
|
||||||
|
<span>{{ client.id }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<label>Room:</label>
|
||||||
|
<span>{{ room.name if room else 'Unknown' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<label>Connected:</label>
|
||||||
|
<span>{{ client.connected_at }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<label>Streaming:</label>
|
||||||
|
<span id="streamingStatus">{{ "Yes" if client.is_streaming else "No" }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Video Controls -->
|
||||||
|
<div class="controls-section">
|
||||||
|
<h3>Video Controls</h3>
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<label>Quality</label>
|
||||||
|
<div class="slider-container">
|
||||||
|
<input type="range" id="qualitySlider" min="10" max="100" value="{{ client.video_settings.quality }}">
|
||||||
|
<span class="slider-value" id="qualityValue">{{ client.video_settings.quality }}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<label>Brightness</label>
|
||||||
|
<div class="slider-container">
|
||||||
|
<input type="range" id="brightnessSlider" min="-100" max="100" value="0">
|
||||||
|
<span class="slider-value" id="brightnessValue">0</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<label>Contrast</label>
|
||||||
|
<div class="slider-container">
|
||||||
|
<input type="range" id="contrastSlider" min="0.5" max="2" step="0.1" value="1">
|
||||||
|
<span class="slider-value" id="contrastValue">1.0</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="btn-group">
|
||||||
|
<button class="btn btn-control" onclick="sendCommand('rotate', {angle: 90})">
|
||||||
|
<i class="fas fa-redo"></i>
|
||||||
|
<span>Rotate 90°</span>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-control" onclick="sendCommand('rotate', {angle: 180})">
|
||||||
|
<i class="fas fa-sync-alt"></i>
|
||||||
|
<span>Rotate 180°</span>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-control" onclick="sendCommand('flip', {direction: 1})">
|
||||||
|
<i class="fas fa-arrows-alt-v"></i>
|
||||||
|
<span>Flip Vertical</span>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-control" onclick="sendCommand('flip', {direction: 0})">
|
||||||
|
<i class="fas fa-arrows-alt-h"></i>
|
||||||
|
<span>Flip Horizontal</span>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-control" onclick="sendCommand('grayscale', {})">
|
||||||
|
<i class="fas fa-adjust"></i>
|
||||||
|
<span>Grayscale</span>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-control" onclick="sendCommand('reset', {})">
|
||||||
|
<i class="fas fa-undo"></i>
|
||||||
|
<span>Reset All</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Stream Information -->
|
||||||
|
<div class="controls-section">
|
||||||
|
<h3>Stream Information</h3>
|
||||||
|
<div class="info-box">
|
||||||
|
<p>This client is connected via WebSocket and streaming video data.</p>
|
||||||
|
<p>Use the controls above to adjust the video stream in real-time.</p>
|
||||||
|
<p>Changes are applied to the video processor on the server side.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- WebSocket Status -->
|
||||||
|
<div class="ws-status" id="wsStatus">
|
||||||
|
<span class="status-indicator status-disconnected"></span>
|
||||||
|
<span id="wsStatusText">Connecting...</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let ws = null;
|
||||||
|
let clientId = '{{ client.id }}';
|
||||||
|
let sessionId = getCookie('session_id');
|
||||||
|
let reconnectAttempts = 0;
|
||||||
|
const maxReconnectAttempts = 5;
|
||||||
|
|
||||||
|
function getCookie(name) {
|
||||||
|
const value = `; ${document.cookie}`;
|
||||||
|
const parts = value.split(`; ${name}=`);
|
||||||
|
if (parts.length === 2) return parts.pop().split(';').shift();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize WebSocket connection
|
||||||
|
function connectWebSocket() {
|
||||||
|
if (!sessionId) {
|
||||||
|
console.error('No session ID found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const wsUrl = `ws://${window.location.hostname}:{{ server_port }}/ws/admin/${sessionId}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
ws = new WebSocket(wsUrl);
|
||||||
|
|
||||||
|
ws.onopen = function() {
|
||||||
|
console.log('WebSocket connected');
|
||||||
|
updateWsStatus(true);
|
||||||
|
reconnectAttempts = 0;
|
||||||
|
|
||||||
|
// Request to watch this client
|
||||||
|
sendCommand('watch_client', { client_id: clientId });
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onmessage = function(event) {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
handleWebSocketMessage(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error parsing WebSocket message:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onclose = function() {
|
||||||
|
console.log('WebSocket disconnected');
|
||||||
|
updateWsStatus(false);
|
||||||
|
|
||||||
|
// Try to reconnect
|
||||||
|
if (reconnectAttempts < maxReconnectAttempts) {
|
||||||
|
reconnectAttempts++;
|
||||||
|
setTimeout(connectWebSocket, 1000 * reconnectAttempts);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onerror = function(error) {
|
||||||
|
console.error('WebSocket error:', error);
|
||||||
|
updateWsStatus(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating WebSocket:', error);
|
||||||
|
updateWsStatus(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update WebSocket status display
|
||||||
|
function updateWsStatus(connected) {
|
||||||
|
const wsStatus = document.getElementById('wsStatus');
|
||||||
|
const wsStatusText = document.getElementById('wsStatusText');
|
||||||
|
|
||||||
|
if (connected) {
|
||||||
|
wsStatus.className = 'ws-status';
|
||||||
|
wsStatusText.innerHTML = '<span class="status-indicator status-connected"></span> Connected';
|
||||||
|
} else {
|
||||||
|
wsStatus.className = 'ws-status';
|
||||||
|
wsStatusText.innerHTML = `<span class="status-indicator status-disconnected"></span> Disconnected (${reconnectAttempts}/${maxReconnectAttempts})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle incoming WebSocket messages
|
||||||
|
function handleWebSocketMessage(data) {
|
||||||
|
console.log('Received:', data);
|
||||||
|
|
||||||
|
switch (data.type) {
|
||||||
|
case 'stream_started':
|
||||||
|
showNotification('Stream started successfully');
|
||||||
|
updateStreamingStatus(true);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'stream_info':
|
||||||
|
showNotification(data.message);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'control_response':
|
||||||
|
if (data.success) {
|
||||||
|
showNotification('Command executed successfully');
|
||||||
|
} else {
|
||||||
|
showNotification('Command failed', 'error');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'stats_update':
|
||||||
|
// Update any stats if needed
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send command to WebSocket
|
||||||
|
function sendCommand(type, data) {
|
||||||
|
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||||||
|
showNotification('WebSocket not connected', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const command = {
|
||||||
|
type: 'control_client',
|
||||||
|
client_id: clientId,
|
||||||
|
command: {
|
||||||
|
type: type,
|
||||||
|
...data
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.send(JSON.stringify(command));
|
||||||
|
console.log('Sent command:', command);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update streaming status
|
||||||
|
function updateStreamingStatus(streaming) {
|
||||||
|
document.getElementById('streamingStatus').textContent = streaming ? 'Yes' : 'No';
|
||||||
|
|
||||||
|
if (streaming) {
|
||||||
|
document.getElementById('videoStats').style.display = 'block';
|
||||||
|
document.getElementById('videoPlaceholder').innerHTML = `
|
||||||
|
<i class="fas fa-sync-alt fa-spin"></i>
|
||||||
|
<h3>Streaming Live Video</h3>
|
||||||
|
<p>Video is being streamed from the client.</p>
|
||||||
|
<p>Use controls to adjust the stream.</p>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show notification
|
||||||
|
function showNotification(message, type = 'success') {
|
||||||
|
// Create notification element
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
padding: 15px 20px;
|
||||||
|
background: ${type === 'error' ? '#ef4444' : '#10b981'};
|
||||||
|
color: white;
|
||||||
|
border-radius: 5px;
|
||||||
|
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
|
||||||
|
z-index: 10000;
|
||||||
|
animation: slideIn 0.3s ease-out;
|
||||||
|
`;
|
||||||
|
notification.textContent = message;
|
||||||
|
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
|
// Remove after 3 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.style.animation = 'slideOut 0.3s ease-in';
|
||||||
|
setTimeout(() => {
|
||||||
|
document.body.removeChild(notification);
|
||||||
|
}, 300);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disconnect client
|
||||||
|
async function disconnectClient() {
|
||||||
|
if (confirm('Are you sure you want to disconnect this client?')) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/disconnect-client/${clientId}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
showNotification('Client disconnected successfully');
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.href = `/room/{{ client.room_id }}`;
|
||||||
|
}, 1000);
|
||||||
|
} else {
|
||||||
|
showNotification('Error: ' + (result.error || 'Failed to disconnect client'), 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showNotification('Error: ' + error.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize sliders
|
||||||
|
function initializeSliders() {
|
||||||
|
// Quality slider
|
||||||
|
const qualitySlider = document.getElementById('qualitySlider');
|
||||||
|
const qualityValue = document.getElementById('qualityValue');
|
||||||
|
|
||||||
|
qualitySlider.addEventListener('input', function() {
|
||||||
|
qualityValue.textContent = this.value + '%';
|
||||||
|
document.getElementById('statsQuality').textContent = this.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
qualitySlider.addEventListener('change', function() {
|
||||||
|
sendCommand('adjust_quality', { quality: parseInt(this.value) });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Brightness slider
|
||||||
|
const brightnessSlider = document.getElementById('brightnessSlider');
|
||||||
|
const brightnessValue = document.getElementById('brightnessValue');
|
||||||
|
|
||||||
|
brightnessSlider.addEventListener('input', function() {
|
||||||
|
brightnessValue.textContent = this.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
brightnessSlider.addEventListener('change', function() {
|
||||||
|
sendCommand('brightness', { value: parseInt(this.value) });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Contrast slider
|
||||||
|
const contrastSlider = document.getElementById('contrastSlider');
|
||||||
|
const contrastValue = document.getElementById('contrastValue');
|
||||||
|
|
||||||
|
contrastSlider.addEventListener('input', function() {
|
||||||
|
contrastValue.textContent = parseFloat(this.value).toFixed(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
contrastSlider.addEventListener('change', function() {
|
||||||
|
sendCommand('contrast', { value: parseFloat(this.value) });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
window.addEventListener('load', function() {
|
||||||
|
if (!sessionId) {
|
||||||
|
window.location.href = '/';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
connectWebSocket();
|
||||||
|
initializeSliders();
|
||||||
|
|
||||||
|
// Add CSS animations
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.textContent = `
|
||||||
|
@keyframes slideIn {
|
||||||
|
from { transform: translateX(100%); opacity: 0; }
|
||||||
|
to { transform: translateX(0); opacity: 1; }
|
||||||
|
}
|
||||||
|
@keyframes slideOut {
|
||||||
|
from { transform: translateX(0); opacity: 1; }
|
||||||
|
to { transform: translateX(100%); opacity: 0; }
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
Reference in New Issue
Block a user