main commit
This commit is contained in:
533
operator-interface.html
Normal file
533
operator-interface.html
Normal file
@@ -0,0 +1,533 @@
|
||||
<!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>
|
||||
Reference in New Issue
Block a user