main commit
This commit is contained in:
@@ -6,7 +6,8 @@ class GodEyeOperator {
|
||||
constructor() {
|
||||
this.socket = null;
|
||||
this.operatorId = uuidv4();
|
||||
this.currentSession = null;
|
||||
this.activeSessions = new Map(); // sessionId -> sessionData
|
||||
this.currentActiveSession = null; // Currently viewing session
|
||||
this.localConnection = null;
|
||||
this.remoteStream = null;
|
||||
this.mediaRecorder = null;
|
||||
@@ -18,6 +19,7 @@ class GodEyeOperator {
|
||||
this.isFullscreen = false;
|
||||
this.isZoomed = false;
|
||||
this.config = null;
|
||||
this.isConnected = false;
|
||||
|
||||
// UI Elements
|
||||
this.elements = {
|
||||
@@ -152,6 +154,56 @@ class GodEyeOperator {
|
||||
this.elements.connectBtn.addEventListener('click', () => this.connect());
|
||||
this.elements.disconnectBtn.addEventListener('click', () => this.disconnect());
|
||||
this.elements.clearLogs.addEventListener('click', () => this.clearLogs());
|
||||
|
||||
// Logs panel toggle
|
||||
const logsToggle = document.getElementById('logs-toggle');
|
||||
const logsContent = document.getElementById('logs-content');
|
||||
const logsCollapseIcon = logsToggle?.querySelector('.collapse-icon');
|
||||
const controlPanel = document.querySelector('.control-panel');
|
||||
|
||||
if (logsToggle) {
|
||||
logsToggle.addEventListener('click', () => {
|
||||
const isCollapsed = logsContent.classList.contains('collapsed');
|
||||
|
||||
if (isCollapsed) {
|
||||
// Разворачиваем
|
||||
logsContent.classList.remove('collapsed');
|
||||
logsCollapseIcon.classList.remove('collapsed');
|
||||
controlPanel.classList.remove('logs-collapsed');
|
||||
logsCollapseIcon.textContent = '▲';
|
||||
} else {
|
||||
// Сворачиваем
|
||||
logsContent.classList.add('collapsed');
|
||||
logsCollapseIcon.classList.add('collapsed');
|
||||
controlPanel.classList.add('logs-collapsed');
|
||||
logsCollapseIcon.textContent = '▼';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Connection panel toggle
|
||||
const connectionToggle = document.getElementById('connection-toggle');
|
||||
const connectionContent = document.getElementById('connection-content');
|
||||
const connectionCollapseIcon = connectionToggle?.querySelector('.collapse-icon');
|
||||
|
||||
if (connectionToggle) {
|
||||
connectionToggle.addEventListener('click', () => {
|
||||
const isCollapsed = connectionContent.classList.contains('collapsed');
|
||||
|
||||
if (isCollapsed) {
|
||||
// Разворачиваем
|
||||
connectionContent.classList.remove('collapsed');
|
||||
connectionCollapseIcon.classList.remove('collapsed');
|
||||
connectionCollapseIcon.textContent = '▲';
|
||||
} else {
|
||||
// Сворачиваем
|
||||
connectionContent.classList.add('collapsed');
|
||||
connectionCollapseIcon.classList.add('collapsed');
|
||||
connectionCollapseIcon.textContent = '▼';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Refresh devices
|
||||
const refreshBtn = document.getElementById('refresh-devices');
|
||||
if (refreshBtn) {
|
||||
@@ -288,6 +340,11 @@ class GodEyeOperator {
|
||||
}
|
||||
|
||||
connect() {
|
||||
if (this.isConnected) {
|
||||
this.disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
const serverUrl = this.elements.serverUrl.value.trim();
|
||||
if (!serverUrl) {
|
||||
this.log('Введите URL сервера', 'error');
|
||||
@@ -321,7 +378,7 @@ class GodEyeOperator {
|
||||
this.log('Отключен от сервера', 'warning');
|
||||
this.updateConnectionStatus(false);
|
||||
this.clearDevicesList();
|
||||
this.clearSessionsList();
|
||||
this.updateSessionsList();
|
||||
this.stopPingMonitoring();
|
||||
});
|
||||
|
||||
@@ -381,6 +438,133 @@ class GodEyeOperator {
|
||||
this.elements.pingIndicator.textContent = `Ping: ${ping}ms`;
|
||||
this.elements.pingIndicator.style.color = ping < 100 ? '#4CAF50' : ping < 300 ? '#FF9800' : '#f44336';
|
||||
});
|
||||
|
||||
// Session events
|
||||
this.socket.on('session:created', (data) => {
|
||||
this.log(`Сессия создана: ${data.sessionId}`, 'info');
|
||||
this.activeSessions.set(data.sessionId, {
|
||||
...data,
|
||||
status: 'pending'
|
||||
});
|
||||
this.updateSessionsList();
|
||||
});
|
||||
|
||||
// Новые события ConnectionManager
|
||||
this.socket.on('connection:initiated', (data) => {
|
||||
this.log(`Подключение инициировано: ${data.connectionId}`, 'info');
|
||||
this.activeSessions.set(data.sessionId, {
|
||||
connectionId: data.connectionId,
|
||||
sessionId: data.sessionId,
|
||||
deviceId: data.deviceId,
|
||||
cameraType: data.cameraType,
|
||||
status: 'pending',
|
||||
createdAt: data.createdAt
|
||||
});
|
||||
this.updateSessionsList();
|
||||
});
|
||||
|
||||
this.socket.on('connection:accepted', (data) => {
|
||||
this.log(`Подключение принято: ${data.connectionId}`, 'success');
|
||||
const session = this.activeSessions.get(data.sessionId);
|
||||
if (session) {
|
||||
session.status = 'active';
|
||||
session.streamUrl = data.streamUrl;
|
||||
if (!this.currentActiveSession) {
|
||||
this.currentActiveSession = data.sessionId;
|
||||
this.currentSession = {
|
||||
id: data.sessionId,
|
||||
deviceId: data.deviceId,
|
||||
cameraType: data.cameraType
|
||||
};
|
||||
this.elements.sessionInfo.textContent = `Активная сессия: ${session.deviceId} (${session.cameraType})`;
|
||||
this.updateCameraButtons(session.cameraType);
|
||||
this.initWebRTC();
|
||||
}
|
||||
this.updateSessionsList();
|
||||
}
|
||||
});
|
||||
|
||||
this.socket.on('connection:rejected', (data) => {
|
||||
this.log(`Подключение отклонено: ${data.sessionId} - ${data.error}`, 'error');
|
||||
const session = this.activeSessions.get(data.sessionId);
|
||||
if (session) {
|
||||
session.status = 'rejected';
|
||||
session.error = data.error;
|
||||
this.updateSessionsList();
|
||||
}
|
||||
});
|
||||
|
||||
this.socket.on('connection:terminated', (data) => {
|
||||
this.log(`Подключение завершено: ${data.connectionId}`, 'info');
|
||||
// Находим сессию по connectionId
|
||||
for (const [sessionId, session] of this.activeSessions.entries()) {
|
||||
if (session.connectionId === data.connectionId) {
|
||||
this.activeSessions.delete(sessionId);
|
||||
if (this.currentActiveSession === sessionId) {
|
||||
this.currentActiveSession = null;
|
||||
this.currentSession = null;
|
||||
this.elements.sessionInfo.textContent = '';
|
||||
this.closeWebRTC();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.updateSessionsList();
|
||||
});
|
||||
|
||||
this.socket.on('connection:error', (data) => {
|
||||
this.log(`Ошибка подключения: ${data.error}`, 'error');
|
||||
});
|
||||
|
||||
// Сохраняем старые события для обратной совместимости
|
||||
this.socket.on('session:created', (data) => {
|
||||
this.log(`Сессия создана: ${data.sessionId}`, 'info');
|
||||
this.activeSessions.set(data.sessionId, {
|
||||
...data,
|
||||
status: 'pending'
|
||||
});
|
||||
this.updateSessionsList();
|
||||
});
|
||||
|
||||
this.socket.on('session:rejected', (data) => {
|
||||
this.log(`Сессия отклонена: ${data.sessionId}`, 'error');
|
||||
const session = this.activeSessions.get(data.sessionId);
|
||||
if (session) {
|
||||
session.status = 'rejected';
|
||||
this.updateSessionsList();
|
||||
}
|
||||
});
|
||||
|
||||
this.socket.on('session:ended', (data) => {
|
||||
this.log(`Сессия завершена: ${data.sessionId}`, 'info');
|
||||
this.activeSessions.delete(data.sessionId);
|
||||
if (this.currentActiveSession === data.sessionId) {
|
||||
this.currentActiveSession = null;
|
||||
this.elements.sessionInfo.textContent = '';
|
||||
}
|
||||
this.updateSessionsList();
|
||||
});
|
||||
|
||||
// Дополнительный обработчик для совместимости
|
||||
this.socket.on('camera:disconnected', (data) => {
|
||||
this.log(`Камера отключена: ${data.sessionId}`, 'warning');
|
||||
const session = this.activeSessions.get(data.sessionId);
|
||||
if (session) {
|
||||
session.status = 'ended';
|
||||
this.updateSessionsList();
|
||||
}
|
||||
});
|
||||
|
||||
// Обработчик обновления списка устройств
|
||||
this.socket.on('device:connected', (data) => {
|
||||
this.log(`Новое устройство подключено: ${data.deviceId}`, 'info');
|
||||
this.requestDevicesList(); // Обновляем список устройств
|
||||
});
|
||||
|
||||
this.socket.on('device:disconnected', (data) => {
|
||||
this.log(`Устройство отключено: ${data.deviceId}`, 'warning');
|
||||
this.requestDevicesList(); // Обновляем список устройств
|
||||
});
|
||||
}
|
||||
|
||||
registerOperator() {
|
||||
@@ -404,18 +588,46 @@ class GodEyeOperator {
|
||||
}
|
||||
|
||||
updateConnectionStatus(connected) {
|
||||
this.elements.connectBtn.disabled = connected;
|
||||
this.elements.disconnectBtn.disabled = !connected;
|
||||
this.isConnected = connected;
|
||||
|
||||
// Обновляем заголовок панели подключения
|
||||
const connectionToggle = document.getElementById('connection-toggle');
|
||||
const connectionTitle = connectionToggle?.querySelector('h3');
|
||||
|
||||
if (connected) {
|
||||
this.elements.connectBtn.textContent = 'Отключиться';
|
||||
this.elements.connectBtn.className = 'btn-danger';
|
||||
this.elements.connectBtn.disabled = false;
|
||||
this.elements.disconnectBtn.disabled = false;
|
||||
this.elements.connectionIndicator.textContent = '● Подключен';
|
||||
this.elements.connectionIndicator.className = 'status connected';
|
||||
this.elements.connectionStatusText.textContent = 'Подключен к серверу';
|
||||
|
||||
// Обновляем заголовок с индикатором подключения
|
||||
if (connectionTitle) {
|
||||
connectionTitle.innerHTML = '🔗 Подключение к серверу <span style="color: #4CAF50;">●</span>';
|
||||
}
|
||||
} else {
|
||||
this.elements.connectBtn.textContent = 'Подключиться';
|
||||
this.elements.connectBtn.className = 'btn-primary';
|
||||
this.elements.connectBtn.disabled = false;
|
||||
this.elements.disconnectBtn.disabled = true;
|
||||
this.elements.connectionIndicator.textContent = '● Отключен';
|
||||
this.elements.connectionIndicator.className = 'status disconnected';
|
||||
this.elements.connectionStatusText.textContent = 'Не подключен';
|
||||
this.elements.sessionInfo.textContent = '';
|
||||
|
||||
// Обновляем заголовок с индикатором отключения
|
||||
if (connectionTitle) {
|
||||
connectionTitle.innerHTML = '🔗 Подключение к серверу <span style="color: #f44336;">●</span>';
|
||||
}
|
||||
|
||||
// Очищаем сессии при отключении
|
||||
if (this.activeSessions) {
|
||||
this.activeSessions.clear();
|
||||
this.currentActiveSession = null;
|
||||
this.updateSessionsList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -431,17 +643,31 @@ class GodEyeOperator {
|
||||
devices.forEach(device => {
|
||||
const deviceElement = document.createElement('div');
|
||||
deviceElement.className = 'device-item';
|
||||
|
||||
// Проверяем, есть ли активные сессии с этим устройством
|
||||
const activeSessions = Array.from(this.activeSessions.values())
|
||||
.filter(session => session.deviceId === device.deviceId && session.status === 'active');
|
||||
|
||||
const hasActiveSessions = activeSessions.length > 0;
|
||||
|
||||
// Сокращенный ID для компактности
|
||||
const shortId = device.deviceId.length > 12 ?
|
||||
device.deviceId.substring(0, 8) + '...' : device.deviceId;
|
||||
|
||||
deviceElement.innerHTML = `
|
||||
<div class="device-info">
|
||||
<strong>ID:</strong> ${device.deviceId}<br>
|
||||
<strong>Статус:</strong> ${device.isConnected ? 'Онлайн' : 'Офлайн'}
|
||||
<strong>ID:</strong> ${shortId}<br>
|
||||
<strong>Статус:</strong> ${device.isConnected ? '🟢 Онлайн' : '🔴 Офлайн'}
|
||||
${hasActiveSessions ? `<br><strong>Сессии:</strong> ${activeSessions.length}` : ''}
|
||||
</div>
|
||||
<div class="device-capabilities">
|
||||
Камеры: ${(Array.isArray(device.capabilities) ? device.capabilities.join(', ') : (device.capabilities?.cameras?.join(', ') || ''))}
|
||||
📷 ${(Array.isArray(device.capabilities) ? device.capabilities.join(', ') : (device.capabilities?.cameras?.join(', ') || 'back'))}
|
||||
</div>
|
||||
<div class="device-actions">
|
||||
<button class="btn-device" onclick="operator.requestCamera('${device.deviceId}', 'back')">
|
||||
Подключиться
|
||||
<button class="btn-device ${hasActiveSessions ? 'btn-success' : 'btn-primary'}"
|
||||
onclick="operator.requestCamera('${device.deviceId}', 'back')"
|
||||
title="${hasActiveSessions ? 'Добавить новую сессию' : 'Подключиться к устройству'}">
|
||||
${hasActiveSessions ? '➕ Добавить' : '🔗 Подключить'}
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
@@ -454,8 +680,129 @@ class GodEyeOperator {
|
||||
this.elements.devicesList.innerHTML = '<div class="no-devices">Нет подключенных устройств</div>';
|
||||
}
|
||||
|
||||
clearSessionsList() {
|
||||
this.elements.sessionsList.innerHTML = '<div class="no-sessions">Нет активных сессий</div>';
|
||||
updateSessionsList() {
|
||||
const container = this.elements.sessionsList;
|
||||
|
||||
if (this.activeSessions.size === 0) {
|
||||
container.innerHTML = '<div class="no-sessions">Нет активных сессий</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = '';
|
||||
|
||||
this.activeSessions.forEach((session, sessionId) => {
|
||||
const sessionElement = document.createElement('div');
|
||||
sessionElement.className = `session-item session-${session.status} ${this.currentActiveSession === sessionId ? 'active' : ''}`;
|
||||
|
||||
const statusText = {
|
||||
'pending': '🟠 Ожидание',
|
||||
'active': '🟢 Активна',
|
||||
'rejected': '🔴 Отклонена',
|
||||
'ended': '⚫ Завершена'
|
||||
};
|
||||
|
||||
// Сокращенный ID устройства
|
||||
const shortDeviceId = session.deviceId.length > 10 ?
|
||||
session.deviceId.substring(0, 8) + '...' : session.deviceId;
|
||||
|
||||
sessionElement.innerHTML = `
|
||||
<div class="session-header">
|
||||
<strong>📱 ${shortDeviceId}</strong> | <strong>📷 ${session.cameraType}</strong><br>
|
||||
<span class="status-${session.status}">${statusText[session.status] || session.status}</span>
|
||||
</div>
|
||||
<div class="session-actions">
|
||||
${session.status === 'active' ? `
|
||||
<button class="btn-small ${this.currentActiveSession === sessionId ? 'btn-success' : 'btn-primary'}"
|
||||
onclick="operator.switchToSession('${sessionId}')"
|
||||
title="${this.currentActiveSession === sessionId ? 'Активная сессия' : 'Переключиться на эту сессию'}">
|
||||
${this.currentActiveSession === sessionId ? '✓ Активна' : '🔄 Переключить'}
|
||||
</button>
|
||||
<button class="btn-small btn-secondary" onclick="operator.switchCamera('${sessionId}', 'front')"
|
||||
title="Переключить на фронтальную камеру">
|
||||
📷
|
||||
</button>
|
||||
${session.connectionId ? `
|
||||
<button class="btn-small btn-warning" onclick="operator.terminateConnection('${session.connectionId}', '${sessionId}')"
|
||||
title="Завершить подключение">
|
||||
🔌❌
|
||||
</button>
|
||||
` : `
|
||||
<button class="btn-small btn-danger" onclick="operator.endSession('${sessionId}')"
|
||||
title="Завершить сессию">
|
||||
❌
|
||||
</button>
|
||||
`}
|
||||
` : `
|
||||
<button class="btn-small btn-secondary" onclick="operator.endSession('${sessionId}')"
|
||||
title="Удалить из списка">
|
||||
🗑️
|
||||
</button>
|
||||
`}
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.appendChild(sessionElement);
|
||||
});
|
||||
}
|
||||
|
||||
switchToSession(sessionId) {
|
||||
const session = this.activeSessions.get(sessionId);
|
||||
if (!session || session.status !== 'active') {
|
||||
this.log(`Нельзя переключиться на сессию ${sessionId}`, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Если это уже активная сессия, ничего не делаем
|
||||
if (this.currentActiveSession === sessionId) {
|
||||
this.log('Эта сессия уже активна', 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentActiveSession = sessionId;
|
||||
|
||||
// Обновляем currentSession для обратной совместимости
|
||||
this.currentSession = {
|
||||
id: sessionId,
|
||||
deviceId: session.deviceId,
|
||||
cameraType: session.cameraType
|
||||
};
|
||||
|
||||
this.log(`Переключение на сессию: ${sessionId} (устройство: ${session.deviceId})`, 'info');
|
||||
this.elements.sessionInfo.textContent = `Активная сессия: ${session.deviceId} (${session.cameraType})`;
|
||||
|
||||
// Обновляем кнопки камеры
|
||||
this.updateCameraButtons(session.cameraType);
|
||||
|
||||
// Обновляем список сессий
|
||||
this.updateSessionsList();
|
||||
|
||||
// TODO: Переключить видеопоток
|
||||
// В будущем здесь будет логика переключения WebRTC потоков
|
||||
}
|
||||
|
||||
switchCamera(sessionId, cameraType) {
|
||||
const session = this.activeSessions.get(sessionId);
|
||||
if (!session || session.status !== 'active') {
|
||||
this.log(`Нельзя переключить камеру в сессии ${sessionId}`, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
this.log(`Переключение камеры в сессии ${sessionId} на ${cameraType}`, 'info');
|
||||
|
||||
if (this.socket) {
|
||||
this.socket.emit('camera:switch', {
|
||||
sessionId: sessionId,
|
||||
cameraType: cameraType
|
||||
});
|
||||
}
|
||||
|
||||
// Обновляем информацию о сессии
|
||||
session.cameraType = cameraType;
|
||||
this.updateSessionsList();
|
||||
|
||||
if (this.currentActiveSession === sessionId) {
|
||||
this.elements.sessionInfo.textContent = `Активная сессия: ${session.deviceId} (${session.cameraType})`;
|
||||
}
|
||||
}
|
||||
|
||||
requestCamera(deviceId, cameraType = 'back') {
|
||||
@@ -466,6 +813,7 @@ class GodEyeOperator {
|
||||
|
||||
this.log(`Запрос доступа к камере ${cameraType} устройства ${deviceId}`, 'info');
|
||||
|
||||
// Используем новое событие для подключения через ConnectionManager
|
||||
this.socket.emit('camera:request', {
|
||||
deviceId: deviceId,
|
||||
operatorId: this.operatorId,
|
||||
@@ -475,11 +823,27 @@ class GodEyeOperator {
|
||||
|
||||
handleCameraResponse(data) {
|
||||
if (data.success) {
|
||||
this.currentSession = data.session;
|
||||
this.elements.sessionInfo.textContent = `Сессия: ${data.session.id}`;
|
||||
this.log(`Доступ к камере получен. Сессия: ${data.session.id}`, 'success');
|
||||
this.updateCameraButtons(data.session.cameraType);
|
||||
this.initWebRTC();
|
||||
// Обновляем сессию в коллекции
|
||||
const sessionData = {
|
||||
sessionId: data.sessionId || data.session.id,
|
||||
deviceId: data.session.deviceId,
|
||||
cameraType: data.session.cameraType,
|
||||
status: 'active'
|
||||
};
|
||||
|
||||
this.activeSessions.set(sessionData.sessionId, sessionData);
|
||||
|
||||
// Если нет текущей активной сессии, сделаем эту активной
|
||||
if (!this.currentActiveSession) {
|
||||
this.currentActiveSession = sessionData.sessionId;
|
||||
this.currentSession = data.session; // Сохраняем для обратной совместимости
|
||||
this.elements.sessionInfo.textContent = `Активная сессия: ${sessionData.deviceId} (${sessionData.cameraType})`;
|
||||
this.updateCameraButtons(sessionData.cameraType);
|
||||
this.initWebRTC();
|
||||
}
|
||||
|
||||
this.log(`Доступ к камере получен. Сессия: ${sessionData.sessionId}`, 'success');
|
||||
this.updateSessionsList();
|
||||
} else {
|
||||
this.log(`Отказ в доступе к камере: ${data.message}`, 'error');
|
||||
}
|
||||
@@ -494,20 +858,106 @@ class GodEyeOperator {
|
||||
});
|
||||
}
|
||||
|
||||
switchCamera(cameraType) {
|
||||
if (!this.currentSession) {
|
||||
switchCamera(sessionIdOrType, cameraType) {
|
||||
// Определяем, передан sessionId или это старый вызов
|
||||
let sessionId, targetCameraType;
|
||||
|
||||
if (cameraType) {
|
||||
// Новый вызов: switchCamera(sessionId, cameraType)
|
||||
sessionId = sessionIdOrType;
|
||||
targetCameraType = cameraType;
|
||||
} else {
|
||||
// Старый вызов: switchCamera(cameraType)
|
||||
targetCameraType = sessionIdOrType;
|
||||
sessionId = this.currentActiveSession;
|
||||
|
||||
if (!sessionId && this.currentSession) {
|
||||
// Обратная совместимость со старым кодом
|
||||
sessionId = this.currentSession.id;
|
||||
}
|
||||
}
|
||||
|
||||
if (!sessionId) {
|
||||
this.log('Нет активной сессии для переключения камеры', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
this.log(`Переключение на камеру: ${cameraType}`, 'info');
|
||||
const session = this.activeSessions.get(sessionId);
|
||||
if (!session || session.status !== 'active') {
|
||||
this.log(`Нельзя переключить камеру в сессии ${sessionId}`, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
this.log(`Переключение на камеру: ${targetCameraType} в сессии ${sessionId}`, 'info');
|
||||
|
||||
this.socket.emit('camera:switch', {
|
||||
sessionId: this.currentSession.id,
|
||||
cameraType: cameraType
|
||||
sessionId: sessionId,
|
||||
cameraType: targetCameraType
|
||||
});
|
||||
|
||||
this.updateCameraButtons(cameraType);
|
||||
// Обновляем состояние кнопок только для активной сессии
|
||||
if (sessionId === this.currentActiveSession) {
|
||||
this.updateCameraButtons(targetCameraType);
|
||||
}
|
||||
}
|
||||
|
||||
// Новые методы для управления подключениями через ConnectionManager
|
||||
|
||||
terminateConnection(connectionId, sessionId) {
|
||||
if (!this.socket || !this.socket.connected) {
|
||||
this.log('Нет подключения к серверу', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
this.log(`Завершение подключения: ${connectionId}`, 'info');
|
||||
|
||||
this.socket.emit('connection:terminate', {
|
||||
connectionId: connectionId
|
||||
});
|
||||
|
||||
// Локально обновляем состояние сессии
|
||||
const session = this.activeSessions.get(sessionId);
|
||||
if (session) {
|
||||
session.status = 'terminating';
|
||||
this.updateSessionsList();
|
||||
}
|
||||
}
|
||||
|
||||
getConnectionStatus() {
|
||||
if (!this.socket || !this.socket.connected) {
|
||||
this.log('Нет подключения к серверу', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
this.socket.emit('connection:status', {}, (response) => {
|
||||
if (response.success) {
|
||||
this.log('Статистика подключений получена', 'info');
|
||||
console.log('Connection Stats:', response.stats);
|
||||
} else {
|
||||
this.log(`Ошибка получения статистики: ${response.error}`, 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
listMyConnections() {
|
||||
if (!this.socket || !this.socket.connected) {
|
||||
this.log('Нет подключения к серверу', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
this.socket.emit('connection:list', {}, (response) => {
|
||||
if (response.success) {
|
||||
this.log(`Получен список подключений: ${response.connections.length}`, 'info');
|
||||
console.log('My Connections:', response.connections);
|
||||
|
||||
// Можно обновить UI с информацией о подключениях
|
||||
response.connections.forEach(conn => {
|
||||
console.log(`Connection ${conn.connectionId}: ${conn.deviceId} -> ${conn.status}`);
|
||||
});
|
||||
} else {
|
||||
this.log(`Ошибка получения списка подключений: ${response.error}`, 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async initWebRTC() {
|
||||
|
||||
@@ -100,52 +100,61 @@
|
||||
<!-- Right Panel - Devices & Sessions -->
|
||||
<div class="control-panel">
|
||||
<!-- Connection Settings -->
|
||||
<div class="connection-panel">
|
||||
<h3>Подключение к серверу</h3>
|
||||
<div class="input-group">
|
||||
<label for="server-url">URL сервера:</label>
|
||||
<input type="text" id="server-url" value="http://localhost:3001" placeholder="http://localhost:3001">
|
||||
<div class="connection-panel panel-section collapsible">
|
||||
<div class="panel-header clickable" id="connection-toggle">
|
||||
<h3>🔌 Подключение к серверу</h3>
|
||||
<span class="collapse-icon collapsed">▼</span>
|
||||
</div>
|
||||
<div class="checkbox-group">
|
||||
<label>
|
||||
<input type="checkbox" id="auto-connect"> Автоматически подключаться при запуске
|
||||
</label>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<button id="connect-btn" class="btn-primary">Подключиться</button>
|
||||
<button id="disconnect-btn" class="btn-secondary" disabled>Отключиться</button>
|
||||
</div>
|
||||
<div id="connection-info" class="connection-info">
|
||||
<span id="connection-status-text">Не подключен</span>
|
||||
<span id="ping-indicator">Ping: --</span>
|
||||
<div class="panel-content collapsed" id="connection-content">
|
||||
<div class="input-group">
|
||||
<label for="server-url">URL сервера:</label>
|
||||
<input type="text" id="server-url" value="http://localhost:3001" placeholder="http://localhost:3001">
|
||||
</div>
|
||||
<div class="checkbox-group">
|
||||
<label>
|
||||
<input type="checkbox" id="auto-connect"> Автоматически подключаться при запуске
|
||||
</label>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<button id="connect-btn" class="btn-primary">Подключиться</button>
|
||||
<button id="disconnect-btn" class="btn-secondary" disabled>Отключиться</button>
|
||||
</div>
|
||||
<div id="connection-info" class="connection-info">
|
||||
<span id="connection-status-text">Не подключен</span>
|
||||
<span id="ping-indicator">Ping: --</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Available Devices -->
|
||||
<div class="devices-panel">
|
||||
<h3>Доступные устройства</h3>
|
||||
<div class="devices-header" style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<span></span>
|
||||
<button id="refresh-devices" class="btn-secondary btn-small" title="Обновить список устройств">🔄 Обновить</button>
|
||||
<div class="devices-panel panel-section">
|
||||
<div class="panel-header">
|
||||
<h3>📱 Доступные устройства</h3>
|
||||
<button id="refresh-devices" class="btn-icon" title="Обновить список">🔄</button>
|
||||
</div>
|
||||
<div id="devices-list" class="devices-list">
|
||||
<div id="devices-list" class="devices-list compact-list">
|
||||
<div class="no-devices">Нет подключенных устройств</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Active Sessions -->
|
||||
<div class="sessions-panel">
|
||||
<h3>Активные сессии</h3>
|
||||
<div id="sessions-list" class="sessions-list">
|
||||
<div class="sessions-panel panel-section">
|
||||
<h3>🔗 Активные сессии</h3>
|
||||
<div id="sessions-list" class="sessions-list compact-list">
|
||||
<div class="no-sessions">Нет активных сессий</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Logs -->
|
||||
<div class="logs-panel">
|
||||
<h3>Журнал событий</h3>
|
||||
<div id="logs-container" class="logs-container" style="max-height: 180px; overflow-y: auto;"></div>
|
||||
<button id="clear-logs" class="btn-secondary btn-small">Очистить</button>
|
||||
<div class="logs-panel panel-section collapsible">
|
||||
<div class="panel-header clickable" id="logs-toggle">
|
||||
<h3>📋 Журнал событий</h3>
|
||||
<span class="collapse-icon">▲</span>
|
||||
</div>
|
||||
<div class="panel-content" id="logs-content">
|
||||
<div id="logs-container" class="logs-container"></div>
|
||||
<button id="clear-logs" class="btn-secondary btn-small">Очистить</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -306,19 +306,91 @@ body {
|
||||
background: #252525;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 350px;
|
||||
max-width: 380px;
|
||||
min-width: 300px;
|
||||
transition: max-width 0.3s ease;
|
||||
}
|
||||
|
||||
.control-panel.logs-collapsed {
|
||||
max-width: 320px;
|
||||
}
|
||||
|
||||
.control-panel > div {
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid #333;
|
||||
}
|
||||
|
||||
/* Panel Sections */
|
||||
.panel-section {
|
||||
padding: 12px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.panel-header.clickable {
|
||||
cursor: pointer;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.panel-header.clickable:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
margin: -5px;
|
||||
padding: 5px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.collapse-icon {
|
||||
font-size: 12px;
|
||||
transition: transform 0.3s ease;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.collapse-icon.collapsed {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.panel-content {
|
||||
transition: all 0.3s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.panel-content.collapsed {
|
||||
max-height: 0;
|
||||
padding: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* Connection Panel */
|
||||
.connection-panel {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.connection-panel.collapsed {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.connection-panel h3 {
|
||||
margin-bottom: 15px;
|
||||
margin-bottom: 8px;
|
||||
color: #4CAF50;
|
||||
font-size: 14px;
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.connection-panel h3 span {
|
||||
font-size: 10px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
@@ -385,32 +457,53 @@ button:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Devices Panel */
|
||||
.devices-panel h3,
|
||||
.sessions-panel h3 {
|
||||
margin-bottom: 15px;
|
||||
color: #2196F3;
|
||||
font-size: 14px;
|
||||
/* Devices and Sessions Panels */
|
||||
.devices-panel,
|
||||
.sessions-panel {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.devices-list,
|
||||
.sessions-list {
|
||||
max-height: 200px;
|
||||
.devices-panel h3,
|
||||
.sessions-panel h3 {
|
||||
margin-bottom: 8px;
|
||||
color: #2196F3;
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.compact-list {
|
||||
max-height: 180px;
|
||||
overflow-y: auto;
|
||||
transition: max-height 0.3s ease;
|
||||
}
|
||||
|
||||
.control-panel.logs-collapsed .compact-list {
|
||||
max-height: 250px;
|
||||
}
|
||||
|
||||
.device-item,
|
||||
.session-item {
|
||||
background: #3a3a3a;
|
||||
margin-bottom: 8px;
|
||||
padding: 10px;
|
||||
margin-bottom: 6px;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
border-left: 3px solid #4CAF50;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.device-item:hover,
|
||||
.session-item:hover {
|
||||
background: #404040;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.device-info {
|
||||
font-size: 12px;
|
||||
margin-bottom: 5px;
|
||||
font-size: 11px;
|
||||
margin-bottom: 4px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.device-info strong {
|
||||
@@ -418,28 +511,56 @@ button:disabled {
|
||||
}
|
||||
|
||||
.device-capabilities {
|
||||
font-size: 11px;
|
||||
font-size: 10px;
|
||||
color: #888;
|
||||
margin-bottom: 8px;
|
||||
margin-bottom: 6px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.device-actions {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
gap: 4px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.btn-device {
|
||||
background: #2196F3;
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 4px 8px;
|
||||
padding: 3px 6px;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
font-size: 10px;
|
||||
font-size: 9px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-device:hover {
|
||||
background: #1976D2;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.btn-device.btn-success {
|
||||
background: #4CAF50;
|
||||
}
|
||||
|
||||
.btn-device.btn-success:hover {
|
||||
background: #45a049;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #888;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
padding: 2px;
|
||||
border-radius: 3px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-icon:hover {
|
||||
color: #fff;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.no-devices,
|
||||
@@ -450,29 +571,119 @@ button:disabled {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* Session styles */
|
||||
.session-item {
|
||||
border-left: 3px solid #2196F3;
|
||||
}
|
||||
|
||||
.session-item.session-pending {
|
||||
border-left-color: #FF9800;
|
||||
}
|
||||
|
||||
.session-item.session-active {
|
||||
border-left-color: #4CAF50;
|
||||
}
|
||||
|
||||
.session-item.session-rejected {
|
||||
border-left-color: #f44336;
|
||||
}
|
||||
|
||||
.session-item.session-ended {
|
||||
border-left-color: #666;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.session-item.active {
|
||||
background: #4a4a4a;
|
||||
border: 2px solid #4CAF50;
|
||||
border-left: 3px solid #4CAF50;
|
||||
}
|
||||
|
||||
.session-header {
|
||||
font-size: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.session-header strong {
|
||||
color: #4CAF50;
|
||||
}
|
||||
|
||||
.status-pending { color: #FF9800; }
|
||||
.status-active { color: #4CAF50; }
|
||||
.status-rejected { color: #f44336; }
|
||||
.status-ended { color: #666; }
|
||||
|
||||
.session-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.btn-small {
|
||||
padding: 2px 6px;
|
||||
font-size: 10px;
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-small.btn-primary {
|
||||
background: #2196F3;
|
||||
}
|
||||
|
||||
.btn-small.btn-success {
|
||||
background: #4CAF50;
|
||||
}
|
||||
|
||||
.btn-small.btn-secondary {
|
||||
background: #757575;
|
||||
}
|
||||
|
||||
.btn-small.btn-danger {
|
||||
background: #f44336;
|
||||
}
|
||||
|
||||
.btn-small:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Logs Panel */
|
||||
.logs-panel {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.logs-panel.collapsed {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.logs-panel h3 {
|
||||
margin-bottom: 15px;
|
||||
margin-bottom: 8px;
|
||||
color: #FF9800;
|
||||
font-size: 14px;
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.logs-container {
|
||||
flex: 1;
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #333;
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
padding: 6px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 11px;
|
||||
font-size: 10px;
|
||||
overflow-y: auto;
|
||||
margin-bottom: 10px;
|
||||
margin-bottom: 8px;
|
||||
max-height: 120px;
|
||||
transition: max-height 0.3s ease;
|
||||
}
|
||||
|
||||
.control-panel.logs-collapsed .logs-container {
|
||||
max-height: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
|
||||
Reference in New Issue
Block a user