Files
radar_frontend/web_viewer/templates/index.html
2025-11-18 12:26:48 +03:00

383 lines
12 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Beacon Tracker - Live Stream</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
}
.container {
max-width: 1400px;
width: 100%;
}
header {
background: rgba(255, 255, 255, 0.95);
padding: 20px 30px;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
margin-bottom: 20px;
}
h1 {
color: #333;
font-size: 2rem;
font-weight: 700;
}
.status-bar {
display: flex;
gap: 20px;
margin-top: 15px;
flex-wrap: wrap;
}
.status-item {
display: flex;
align-items: center;
gap: 8px;
}
.status-indicator {
width: 12px;
height: 12px;
border-radius: 50%;
background: #e74c3c;
animation: pulse 2s ease-in-out infinite;
}
.status-indicator.connected {
background: #2ecc71;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.status-text {
color: #555;
font-size: 0.95rem;
}
.main-content {
background: rgba(255, 255, 255, 0.95);
border-radius: 15px;
padding: 30px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
}
.video-container {
position: relative;
background: #000;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
}
#videoFrame {
width: 100%;
height: auto;
display: block;
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.8);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: white;
}
.loading-overlay.hidden {
display: none;
}
.spinner {
width: 50px;
height: 50px;
border: 4px solid rgba(255, 255, 255, 0.3);
border-top-color: white;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 15px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-top: 20px;
}
.info-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 15px;
border-radius: 10px;
color: white;
}
.info-label {
font-size: 0.85rem;
opacity: 0.9;
margin-bottom: 5px;
}
.info-value {
font-size: 1.5rem;
font-weight: 700;
}
.error-message {
background: #e74c3c;
color: white;
padding: 15px;
border-radius: 10px;
margin-top: 15px;
display: none;
}
.error-message.visible {
display: block;
}
.controls {
margin-top: 20px;
display: flex;
gap: 10px;
flex-wrap: wrap;
}
button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 12px 24px;
border-radius: 8px;
font-size: 1rem;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
button:active {
transform: translateY(0);
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>🎯 Beacon Tracker - Live Stream</h1>
<div class="status-bar">
<div class="status-item">
<div class="status-indicator" id="connectionStatus"></div>
<span class="status-text" id="connectionText">Подключение...</span>
</div>
<div class="status-item">
<span class="status-text">FPS: <strong id="fpsCounter">0</strong></span>
</div>
<div class="status-item">
<span class="status-text">Кадр: <strong id="frameCounter">0</strong></span>
</div>
</div>
</header>
<div class="main-content">
<div class="video-container">
<img id="videoFrame" alt="Video stream">
<div class="loading-overlay" id="loadingOverlay">
<div class="spinner"></div>
<p>Ожидание данных...</p>
</div>
</div>
<div class="info-grid">
<div class="info-card">
<div class="info-label">Разрешение</div>
<div class="info-value" id="resolution">-</div>
</div>
<div class="info-card">
<div class="info-label">Размер кадра</div>
<div class="info-value" id="frameSize">-</div>
</div>
<div class="info-card">
<div class="info-label">Задержка</div>
<div class="info-value" id="latency">-</div>
</div>
<div class="info-card">
<div class="info-label">Битрейт</div>
<div class="info-value" id="bitrate">-</div>
</div>
</div>
<div class="controls">
<button id="reconnectBtn" onclick="reconnect()">🔄 Переподключиться</button>
<button id="screenshotBtn" onclick="takeScreenshot()">📸 Скриншот</button>
</div>
<div class="error-message" id="errorMessage"></div>
</div>
</div>
<script>
let eventSource = null;
let frameCount = 0;
let lastFrameTime = Date.now();
let fps = 0;
let totalBytes = 0;
let lastBitrateCalc = Date.now();
function connect() {
if (eventSource) {
eventSource.close();
}
eventSource = new EventSource('/stream');
eventSource.onopen = () => {
updateConnectionStatus(true);
hideError();
};
eventSource.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
if (data.error) {
showError(data.error);
return;
}
// Update frame
const img = document.getElementById('videoFrame');
img.src = 'data:image/jpeg;base64,' + data.jpeg;
// Hide loading overlay on first frame
document.getElementById('loadingOverlay').classList.add('hidden');
// Update counters
frameCount++;
document.getElementById('frameCounter').textContent = data.frame_number;
// Calculate FPS
const now = Date.now();
const timeDiff = (now - lastFrameTime) / 1000;
if (timeDiff > 0) {
fps = Math.round(1 / timeDiff);
document.getElementById('fpsCounter').textContent = fps;
}
lastFrameTime = now;
// Update resolution
document.getElementById('resolution').textContent = `${data.width}×${data.height}`;
// Update frame size
const sizeKB = (data.data_size / 1024).toFixed(1);
document.getElementById('frameSize').textContent = `${sizeKB} KB`;
// Calculate latency
const currentTimeUs = Date.now() * 1000;
const latencyMs = Math.round((currentTimeUs - data.timestamp_us) / 1000);
document.getElementById('latency').textContent = `${latencyMs} ms`;
// Calculate bitrate
totalBytes += data.data_size;
const bitrateTime = (now - lastBitrateCalc) / 1000;
if (bitrateTime >= 1.0) {
const bitrate = (totalBytes * 8 / bitrateTime / 1000000).toFixed(2);
document.getElementById('bitrate').textContent = `${bitrate} Mbps`;
totalBytes = 0;
lastBitrateCalc = now;
}
} catch (error) {
console.error('Error processing frame:', error);
showError('Ошибка обработки кадра: ' + error.message);
}
};
eventSource.onerror = (error) => {
console.error('EventSource error:', error);
updateConnectionStatus(false);
showError('Потеряно соединение с сервером');
};
}
function updateConnectionStatus(connected) {
const indicator = document.getElementById('connectionStatus');
const text = document.getElementById('connectionText');
if (connected) {
indicator.classList.add('connected');
text.textContent = 'Подключено';
} else {
indicator.classList.remove('connected');
text.textContent = 'Отключено';
}
}
function showError(message) {
const errorDiv = document.getElementById('errorMessage');
errorDiv.textContent = message;
errorDiv.classList.add('visible');
}
function hideError() {
const errorDiv = document.getElementById('errorMessage');
errorDiv.classList.remove('visible');
}
function reconnect() {
document.getElementById('loadingOverlay').classList.remove('hidden');
connect();
}
function takeScreenshot() {
const img = document.getElementById('videoFrame');
const link = document.createElement('a');
link.download = `beacon_tracker_${Date.now()}.jpg`;
link.href = img.src;
link.click();
}
// Connect on page load
connect();
</script>
</body>
</html>