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