534 lines
18 KiB
HTML
534 lines
18 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>GodEye - Интерфейс Оператора</title>
|
||
<style>
|
||
body {
|
||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||
margin: 0;
|
||
padding: 20px;
|
||
background: #1a1a1a;
|
||
color: #fff;
|
||
}
|
||
|
||
.container {
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.header {
|
||
text-align: center;
|
||
margin-bottom: 30px;
|
||
}
|
||
|
||
.status {
|
||
padding: 15px;
|
||
border-radius: 8px;
|
||
margin: 10px 0;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.status.disconnected { background: #d32f2f; }
|
||
.status.connecting { background: #f57c00; }
|
||
.status.connected { background: #388e3c; }
|
||
|
||
.video-container {
|
||
position: relative;
|
||
background: #000;
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
margin: 20px 0;
|
||
}
|
||
|
||
#remoteVideo {
|
||
width: 100%;
|
||
height: 480px;
|
||
object-fit: cover;
|
||
}
|
||
|
||
.controls {
|
||
display: flex;
|
||
gap: 10px;
|
||
margin: 20px 0;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
button {
|
||
padding: 12px 24px;
|
||
border: none;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
font-weight: bold;
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.btn-primary { background: #2196f3; color: white; }
|
||
.btn-success { background: #4caf50; color: white; }
|
||
.btn-danger { background: #f44336; color: white; }
|
||
.btn-warning { background: #ff9800; color: white; }
|
||
|
||
button:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
|
||
}
|
||
|
||
.device-list {
|
||
background: #2a2a2a;
|
||
border-radius: 8px;
|
||
padding: 20px;
|
||
margin: 20px 0;
|
||
}
|
||
|
||
.device-item {
|
||
background: #3a3a3a;
|
||
padding: 15px;
|
||
border-radius: 6px;
|
||
margin: 10px 0;
|
||
cursor: pointer;
|
||
transition: background 0.3s;
|
||
}
|
||
|
||
.device-item:hover {
|
||
background: #4a4a4a;
|
||
}
|
||
|
||
.logs {
|
||
background: #0a0a0a;
|
||
border-radius: 8px;
|
||
padding: 15px;
|
||
height: 200px;
|
||
overflow-y: auto;
|
||
font-family: 'Courier New', monospace;
|
||
font-size: 12px;
|
||
margin: 20px 0;
|
||
}
|
||
|
||
.log-entry {
|
||
margin: 2px 0;
|
||
padding: 2px 0;
|
||
}
|
||
|
||
.log-info { color: #81c784; }
|
||
.log-warning { color: #ffb74d; }
|
||
.log-error { color: #e57373; }
|
||
|
||
.stats {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||
gap: 15px;
|
||
margin: 20px 0;
|
||
}
|
||
|
||
.stat-card {
|
||
background: #2a2a2a;
|
||
padding: 15px;
|
||
border-radius: 8px;
|
||
text-align: center;
|
||
}
|
||
|
||
.stat-value {
|
||
font-size: 24px;
|
||
font-weight: bold;
|
||
color: #2196f3;
|
||
}
|
||
|
||
.network-monitor {
|
||
background: #2a2a2a;
|
||
border-radius: 8px;
|
||
padding: 20px;
|
||
margin: 20px 0;
|
||
}
|
||
|
||
.traffic-indicator {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
margin: 10px 0;
|
||
}
|
||
|
||
.traffic-bar {
|
||
flex: 1;
|
||
height: 8px;
|
||
background: #1a1a1a;
|
||
border-radius: 4px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.traffic-fill {
|
||
height: 100%;
|
||
transition: width 0.3s;
|
||
}
|
||
|
||
.upload { background: #4caf50; }
|
||
.download { background: #2196f3; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<div class="header">
|
||
<h1>🕵️ GodEye - Интерфейс Оператора</h1>
|
||
<div id="connectionStatus" class="status disconnected">
|
||
🔴 Не подключен к серверу сигналинга
|
||
</div>
|
||
</div>
|
||
|
||
<div class="controls">
|
||
<button id="connectBtn" class="btn-primary" onclick="connectToSignaling()">
|
||
📡 Подключиться к серверу
|
||
</button>
|
||
<button id="refreshBtn" class="btn-warning" onclick="refreshDevices()">
|
||
🔄 Обновить устройства
|
||
</button>
|
||
<button id="disconnectBtn" class="btn-danger" onclick="disconnect()" disabled>
|
||
🔌 Отключиться
|
||
</button>
|
||
</div>
|
||
|
||
<div class="device-list">
|
||
<h3>📱 Доступные устройства</h3>
|
||
<div id="deviceList">
|
||
<p>Нет доступных устройств. Убедитесь, что приложение запущено на устройстве.</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="video-container">
|
||
<video id="remoteVideo" autoplay playsinline muted>
|
||
<p>Видео поток будет отображен здесь после подключения к устройству</p>
|
||
</video>
|
||
</div>
|
||
|
||
<div class="stats">
|
||
<div class="stat-card">
|
||
<div class="stat-value" id="connectionTime">--:--</div>
|
||
<div>Время подключения</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-value" id="videoQuality">---</div>
|
||
<div>Качество видео</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-value" id="latency">--- ms</div>
|
||
<div>Задержка</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-value" id="bandwidth">--- kbps</div>
|
||
<div>Пропускная способность</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="network-monitor">
|
||
<h3>📊 Мониторинг сети</h3>
|
||
<div class="traffic-indicator">
|
||
<span>📤 Исходящий:</span>
|
||
<div class="traffic-bar">
|
||
<div id="uploadBar" class="traffic-fill upload" style="width: 0%"></div>
|
||
</div>
|
||
<span id="uploadSpeed">0 KB/s</span>
|
||
</div>
|
||
<div class="traffic-indicator">
|
||
<span>📥 Входящий:</span>
|
||
<div class="traffic-bar">
|
||
<div id="downloadBar" class="traffic-fill download" style="width: 0%"></div>
|
||
</div>
|
||
<span id="downloadSpeed">0 KB/s</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="logs">
|
||
<div id="logContainer"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// Глобальные переменные
|
||
let ws = null;
|
||
let pc = null;
|
||
let currentSession = null;
|
||
let connectionStartTime = null;
|
||
let statsInterval = null;
|
||
|
||
// WebRTC конфигурация
|
||
const pcConfig = {
|
||
iceServers: [
|
||
{ urls: 'stun:stun.l.google.com:19302' },
|
||
{ urls: 'stun:stun1.l.google.com:19302' }
|
||
]
|
||
};
|
||
|
||
// Функции логирования
|
||
function log(message, type = 'info') {
|
||
const logContainer = document.getElementById('logContainer');
|
||
const timestamp = new Date().toLocaleTimeString();
|
||
const logEntry = document.createElement('div');
|
||
logEntry.className = `log-entry log-${type}`;
|
||
logEntry.textContent = `[${timestamp}] ${message}`;
|
||
logContainer.appendChild(logEntry);
|
||
logContainer.scrollTop = logContainer.scrollHeight;
|
||
console.log(message);
|
||
}
|
||
|
||
// Подключение к серверу сигналинга
|
||
function connectToSignaling() {
|
||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||
log('Уже подключен к серверу сигналинга', 'warning');
|
||
return;
|
||
}
|
||
|
||
log('Подключение к серверу сигналинга...', 'info');
|
||
updateConnectionStatus('connecting', '🟡 Подключение к серверу...');
|
||
|
||
ws = new WebSocket('ws://localhost:8765');
|
||
|
||
ws.onopen = function() {
|
||
log('✅ Подключен к серверу сигналинга', 'info');
|
||
updateConnectionStatus('connected', '🟢 Подключен к серверу сигналинга');
|
||
document.getElementById('connectBtn').disabled = true;
|
||
document.getElementById('disconnectBtn').disabled = false;
|
||
refreshDevices();
|
||
};
|
||
|
||
ws.onmessage = function(event) {
|
||
const data = JSON.parse(event.data);
|
||
handleSignalingMessage(data);
|
||
};
|
||
|
||
ws.onclose = function() {
|
||
log('❌ Соединение с сервером разорвано', 'error');
|
||
updateConnectionStatus('disconnected', '🔴 Не подключен к серверу');
|
||
document.getElementById('connectBtn').disabled = false;
|
||
document.getElementById('disconnectBtn').disabled = true;
|
||
};
|
||
|
||
ws.onerror = function(error) {
|
||
log('❌ Ошибка подключения к серверу: ' + error, 'error');
|
||
updateConnectionStatus('disconnected', '🔴 Ошибка подключения');
|
||
};
|
||
}
|
||
|
||
// Обработка сообщений сигналинга
|
||
function handleSignalingMessage(data) {
|
||
log(`📨 Получено сообщение: ${data.type}`, 'info');
|
||
|
||
switch(data.type) {
|
||
case 'client_registered':
|
||
log(`🆔 Зарегистрирован как клиент: ${data.client_id}`, 'info');
|
||
break;
|
||
|
||
case 'session_joined':
|
||
currentSession = data.session_id;
|
||
log(`✅ Подключен к сессии: ${data.session_id}`, 'info');
|
||
log(`📱 Устройство: ${data.device_info.model || 'Unknown'}`, 'info');
|
||
connectionStartTime = Date.now();
|
||
startStatsMonitoring();
|
||
break;
|
||
|
||
case 'offer':
|
||
handleOffer(data.sdp);
|
||
break;
|
||
|
||
case 'ice_candidate':
|
||
handleIceCandidate(data.candidate);
|
||
break;
|
||
|
||
case 'hangup':
|
||
handleHangup();
|
||
break;
|
||
|
||
case 'error':
|
||
log(`❌ Ошибка: ${data.message}`, 'error');
|
||
break;
|
||
}
|
||
}
|
||
|
||
// Обработка SDP offer
|
||
async function handleOffer(offer) {
|
||
try {
|
||
log('📞 Получен SDP Offer, создание PeerConnection...', 'info');
|
||
|
||
pc = new RTCPeerConnection(pcConfig);
|
||
|
||
pc.onicecandidate = function(event) {
|
||
if (event.candidate) {
|
||
log('🧊 Отправка ICE candidate', 'info');
|
||
sendMessage({
|
||
type: 'ice_candidate',
|
||
session_id: currentSession,
|
||
candidate: event.candidate
|
||
});
|
||
}
|
||
};
|
||
|
||
pc.ontrack = function(event) {
|
||
log('📹 Получен видео поток', 'info');
|
||
const remoteVideo = document.getElementById('remoteVideo');
|
||
remoteVideo.srcObject = event.streams[0];
|
||
};
|
||
|
||
await pc.setRemoteDescription(offer);
|
||
const answer = await pc.createAnswer();
|
||
await pc.setLocalDescription(answer);
|
||
|
||
log('📞 Отправка SDP Answer', 'info');
|
||
sendMessage({
|
||
type: 'answer',
|
||
session_id: currentSession,
|
||
sdp: answer
|
||
});
|
||
|
||
} catch (error) {
|
||
log(`❌ Ошибка обработки offer: ${error}`, 'error');
|
||
}
|
||
}
|
||
|
||
// Обработка ICE candidate
|
||
async function handleIceCandidate(candidate) {
|
||
try {
|
||
if (pc) {
|
||
await pc.addIceCandidate(candidate);
|
||
log('🧊 ICE candidate добавлен', 'info');
|
||
}
|
||
} catch (error) {
|
||
log(`❌ Ошибка добавления ICE candidate: ${error}`, 'error');
|
||
}
|
||
}
|
||
|
||
// Подключение к устройству
|
||
function connectToDevice(sessionId) {
|
||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||
log('❌ Нет подключения к серверу сигналинга', 'error');
|
||
return;
|
||
}
|
||
|
||
log(`🔗 Подключение к устройству в сессии: ${sessionId}`, 'info');
|
||
|
||
sendMessage({
|
||
type: 'join_session',
|
||
session_id: sessionId
|
||
});
|
||
}
|
||
|
||
// Отправка сообщения через WebSocket
|
||
function sendMessage(message) {
|
||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||
ws.send(JSON.stringify(message));
|
||
}
|
||
}
|
||
|
||
// Обновление статуса подключения
|
||
function updateConnectionStatus(status, text) {
|
||
const statusElement = document.getElementById('connectionStatus');
|
||
statusElement.className = `status ${status}`;
|
||
statusElement.textContent = text;
|
||
}
|
||
|
||
// Обновление списка устройств
|
||
function refreshDevices() {
|
||
const deviceList = document.getElementById('deviceList');
|
||
deviceList.innerHTML = '<p>🔍 Поиск доступных устройств...</p>';
|
||
|
||
// Имитация поиска устройств (в реальности будет запрос к серверу)
|
||
setTimeout(() => {
|
||
deviceList.innerHTML = `
|
||
<div class="device-item" onclick="connectToDevice('demo-session-1')">
|
||
<strong>📱 LG G6 (LGMG600S9b4da66b)</strong><br>
|
||
<small>Android 8.0 • IP: 192.168.1.100 • Последняя активность: только что</small>
|
||
</div>
|
||
`;
|
||
}, 1000);
|
||
}
|
||
|
||
// Завершение соединения
|
||
function disconnect() {
|
||
if (pc) {
|
||
pc.close();
|
||
pc = null;
|
||
}
|
||
|
||
if (currentSession) {
|
||
sendMessage({
|
||
type: 'hangup',
|
||
session_id: currentSession
|
||
});
|
||
currentSession = null;
|
||
}
|
||
|
||
if (ws) {
|
||
ws.close();
|
||
ws = null;
|
||
}
|
||
|
||
if (statsInterval) {
|
||
clearInterval(statsInterval);
|
||
statsInterval = null;
|
||
}
|
||
|
||
connectionStartTime = null;
|
||
updateConnectionStatus('disconnected', '🔴 Отключен');
|
||
document.getElementById('remoteVideo').srcObject = null;
|
||
|
||
log('🔌 Соединение разорвано', 'info');
|
||
}
|
||
|
||
// Обработка завершения сессии
|
||
function handleHangup() {
|
||
log('📴 Сессия завершена', 'info');
|
||
disconnect();
|
||
}
|
||
|
||
// Мониторинг статистики
|
||
function startStatsMonitoring() {
|
||
if (statsInterval) {
|
||
clearInterval(statsInterval);
|
||
}
|
||
|
||
statsInterval = setInterval(updateStats, 1000);
|
||
}
|
||
|
||
function updateStats() {
|
||
// Обновление времени подключения
|
||
if (connectionStartTime) {
|
||
const elapsed = Math.floor((Date.now() - connectionStartTime) / 1000);
|
||
const minutes = Math.floor(elapsed / 60);
|
||
const seconds = elapsed % 60;
|
||
document.getElementById('connectionTime').textContent =
|
||
`${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
||
}
|
||
|
||
// WebRTC статистика (упрощенная)
|
||
if (pc) {
|
||
pc.getStats().then(stats => {
|
||
stats.forEach(report => {
|
||
if (report.type === 'inbound-rtp' && report.mediaType === 'video') {
|
||
const fps = report.framesPerSecond || 0;
|
||
const bitrate = Math.round((report.bytesReceived || 0) * 8 / 1000);
|
||
|
||
document.getElementById('videoQuality').textContent = `${fps} FPS`;
|
||
document.getElementById('bandwidth').textContent = `${bitrate} kbps`;
|
||
}
|
||
});
|
||
});
|
||
}
|
||
}
|
||
|
||
// Автоподключение при загрузке страницы
|
||
window.onload = function() {
|
||
log('🚀 GodEye Operator Interface загружен', 'info');
|
||
|
||
// Попытка автоподключения через 1 секунду
|
||
setTimeout(() => {
|
||
log('🔄 Попытка автоподключения к серверу...', 'info');
|
||
connectToSignaling();
|
||
}, 1000);
|
||
};
|
||
|
||
// Обработка закрытия окна
|
||
window.onbeforeunload = function() {
|
||
disconnect();
|
||
};
|
||
</script>
|
||
</body>
|
||
</html>
|