init commit

This commit is contained in:
2025-12-03 18:55:22 +09:00
commit f05e8e728f
12 changed files with 6325 additions and 0 deletions

12
.dockerignore Normal file
View File

@@ -0,0 +1,12 @@
__pycache__/
*.pyc
.pytest_cache/
.venv/
venv/
env/
.env
.git/
node_modules/
*.log
build/
dist/

7
.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
.venv/
venv/
env/
.env
.git/
__pycache__/
.history/

31
Dockerfile Normal file
View 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
View 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
View 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
View File

@@ -0,0 +1,11 @@
fastapi
uvicorn
opencv-python
psutil
numpy
jinja2
uvicorn[standard]
typing-extensions
aiofiles
websockets
python-multipart

3724
server.py Normal file

File diff suppressed because it is too large Load Diff

450
templates/create_room.html Normal file
View 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
View 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
View 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
View 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
View 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>