Files
god_eye_android/operator-interface.html
2025-10-06 09:40:51 +09:00

534 lines
18 KiB
HTML
Raw Permalink 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>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>