connection fixes

This commit is contained in:
2025-10-06 09:41:23 +09:00
parent 4ceccae6ce
commit fa55367e68
361 changed files with 24633 additions and 6206 deletions

View File

@@ -1,815 +0,0 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>GodEye Mobile Camera</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #000;
color: #fff;
overflow: hidden;
touch-action: manipulation;
}
.mobile-container {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
}
.header {
background: rgba(0, 0, 0, 0.8);
padding: 10px 15px;
display: flex;
justify-content: space-between;
align-items: center;
z-index: 100;
backdrop-filter: blur(10px);
}
.header h1 {
font-size: 18px;
color: #4CAF50;
}
.status {
font-size: 12px;
padding: 4px 8px;
border-radius: 12px;
background: #333;
}
.status.connected {
background: #4CAF50;
color: #000;
}
.status.disconnected {
background: #f44336;
}
.video-container {
flex: 1;
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
#localVideo {
width: 100%;
height: 100%;
object-fit: cover;
transform: scaleX(-1); /* Зеркало для фронтальной камеры */
}
.no-camera {
text-align: center;
padding: 20px;
}
.controls {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 15px;
background: rgba(0, 0, 0, 0.7);
padding: 15px 20px;
border-radius: 25px;
backdrop-filter: blur(10px);
}
.btn {
width: 50px;
height: 50px;
border: none;
border-radius: 50%;
font-size: 20px;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
}
.btn-camera {
background: #4CAF50;
color: white;
}
.btn-camera:hover {
background: #45a049;
transform: scale(1.1);
}
.btn-camera:disabled {
background: #666;
cursor: not-allowed;
transform: none;
}
.btn-switch {
background: #2196F3;
color: white;
}
.btn-switch:hover {
background: #1976D2;
transform: scale(1.1);
}
.btn-disconnect {
background: #f44336;
color: white;
}
.btn-disconnect:hover {
background: #d32f2f;
transform: scale(1.1);
}
.device-info {
position: absolute;
top: 80px;
left: 15px;
background: rgba(0, 0, 0, 0.7);
padding: 10px;
border-radius: 8px;
font-size: 12px;
backdrop-filter: blur(10px);
}
.requests-panel {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0, 0, 0, 0.9);
padding: 20px;
border-radius: 12px;
text-align: center;
display: none;
backdrop-filter: blur(10px);
border: 1px solid #333;
}
.requests-panel.visible {
display: block;
}
.request-buttons {
display: flex;
gap: 10px;
margin-top: 15px;
}
.btn-accept {
background: #4CAF50;
color: white;
padding: 10px 20px;
border: none;
border-radius: 8px;
font-size: 14px;
cursor: pointer;
}
.btn-reject {
background: #f44336;
color: white;
padding: 10px 20px;
border: none;
border-radius: 8px;
font-size: 14px;
cursor: pointer;
}
.logs {
position: absolute;
bottom: 120px;
left: 15px;
right: 15px;
background: rgba(0, 0, 0, 0.8);
padding: 10px;
border-radius: 8px;
font-size: 11px;
max-height: 100px;
overflow-y: auto;
backdrop-filter: blur(10px);
display: none;
}
.logs.visible {
display: block;
}
.log-entry {
margin-bottom: 2px;
opacity: 0.8;
}
.log-entry.info { color: #2196F3; }
.log-entry.success { color: #4CAF50; }
.log-entry.warning { color: #FF9800; }
.log-entry.error { color: #f44336; }
@media (orientation: landscape) {
.controls {
right: 20px;
left: auto;
transform: none;
flex-direction: column;
}
.device-info {
top: 15px;
left: 15px;
}
}
</style>
</head>
<body>
<div class="mobile-container">
<div class="header">
<h1>📱 GodEye Mobile</h1>
<div id="status" class="status disconnected">Отключено</div>
</div>
<div class="video-container">
<video id="localVideo" autoplay playsinline muted></video>
<div id="no-camera" class="no-camera">
<h3>📷 Камера не активна</h3>
<p>Нажмите кнопку включения камеры</p>
</div>
</div>
<div class="device-info" id="device-info">
<div>📱 <strong>Device ID:</strong> <span id="device-id">Генерация...</span></div>
<div>📷 <strong>Camera:</strong> <span id="current-camera">none</span></div>
<div>🔗 <strong>Sessions:</strong> <span id="session-count">0</span></div>
</div>
<div class="controls">
<button id="btn-camera" class="btn btn-camera" title="Включить/выключить камеру">
📷
</button>
<button id="btn-switch" class="btn btn-switch" title="Переключить камеру">
🔄
</button>
<button id="btn-disconnect" class="btn btn-disconnect" title="Отключиться">
</button>
</div>
<div id="requests-panel" class="requests-panel">
<h3>📞 Запрос на доступ к камере</h3>
<p>Оператор <strong id="operator-id"></strong> запрашивает доступ к камере</p>
<div class="request-buttons">
<button id="btn-accept" class="btn-accept">✅ Принять</button>
<button id="btn-reject" class="btn-reject">❌ Отклонить</button>
</div>
</div>
<div id="logs" class="logs">
<!-- Логи здесь -->
</div>
</div>
<script src="/socket.io/socket.io.js"></script>
<script>
class MobileCameraApp {
constructor() {
this.socket = null;
this.deviceId = this.generateDeviceId();
this.localStream = null;
this.currentCamera = 'back';
this.activeSessions = new Map();
this.pendingRequests = new Map();
this.peerConnections = new Map(); // WebRTC connections
this.logTimeout = null;
this.isConnected = false;
this.isCameraOn = false;
this.elements = {
status: document.getElementById('status'),
localVideo: document.getElementById('localVideo'),
noCamera: document.getElementById('no-camera'),
deviceId: document.getElementById('device-id'),
currentCamera: document.getElementById('current-camera'),
sessionCount: document.getElementById('session-count'),
requestsPanel: document.getElementById('requests-panel'),
operatorId: document.getElementById('operator-id'),
logs: document.getElementById('logs'),
btnCamera: document.getElementById('btn-camera'),
btnSwitch: document.getElementById('btn-switch'),
btnDisconnect: document.getElementById('btn-disconnect'),
btnAccept: document.getElementById('btn-accept'),
btnReject: document.getElementById('btn-reject')
};
this.init();
}
generateDeviceId() {
// Используем комбинацию для мобильного устройства
const timestamp = Date.now().toString(36);
const random = Math.random().toString(36).substr(2, 9);
return `mobile_${timestamp}_${random}`;
}
init() {
this.elements.deviceId.textContent = this.deviceId;
this.setupEventListeners();
this.connectToServer();
}
setupEventListeners() {
this.elements.btnCamera.addEventListener('click', () => this.toggleCamera());
this.elements.btnSwitch.addEventListener('click', () => this.switchCamera());
this.elements.btnDisconnect.addEventListener('click', () => this.disconnect());
this.elements.btnAccept.addEventListener('click', () => this.acceptRequest());
this.elements.btnReject.addEventListener('click', () => this.rejectRequest());
// Обработка поворота экрана
window.addEventListener('orientationchange', () => {
setTimeout(() => this.adjustLayout(), 100);
});
}
connectToServer() {
this.log('Подключение к серверу...', 'info');
this.socket = io();
this.socket.on('connect', () => {
this.log('✅ Подключено к серверу', 'success');
this.isConnected = true;
this.updateStatus('connected');
this.registerDevice();
});
this.socket.on('disconnect', () => {
this.log('❌ Отключено от сервера', 'warning');
this.isConnected = false;
this.updateStatus('disconnected');
this.stopCamera();
});
this.socket.on('camera:request', (data) => {
this.handleCameraRequest(data);
});
this.socket.on('camera:switch', (data) => {
this.handleCameraSwitchRequest(data);
});
this.socket.on('camera:disconnect', (data) => {
this.handleDisconnectRequest(data);
});
// WebRTC обработчики
this.socket.on('webrtc:offer', (data) => {
this.handleWebRTCOffer(data);
});
this.socket.on('webrtc:answer', (data) => {
this.handleWebRTCAnswer(data);
});
this.socket.on('webrtc:ice-candidate', (data) => {
this.handleWebRTCIceCandidate(data);
});
this.socket.on('register:success', (data) => {
this.log(`📱 Устройство зарегистрировано: ${data.deviceId}`, 'success');
});
this.socket.on('register:error', (error) => {
this.log(`❌ Ошибка регистрации: ${error.message}`, 'error');
});
}
registerDevice() {
const deviceInfo = {
platform: 'mobile_web',
userAgent: navigator.userAgent,
cameras: ['back', 'front'],
capabilities: {
video: true,
audio: false
}
};
this.socket.emit('register:mobile_web', {
deviceId: this.deviceId,
deviceInfo: deviceInfo
});
}
async toggleCamera() {
if (this.isCameraOn) {
this.stopCamera();
} else {
await this.startCamera();
}
}
async startCamera() {
try {
this.log(`📷 Запуск камеры (${this.currentCamera})...`, 'info');
const constraints = {
video: {
facingMode: this.currentCamera === 'front' ? 'user' : 'environment',
width: { ideal: 1280 },
height: { ideal: 720 }
},
audio: false
};
this.localStream = await navigator.mediaDevices.getUserMedia(constraints);
this.elements.localVideo.srcObject = this.localStream;
this.isCameraOn = true;
this.elements.btnCamera.textContent = '📷';
this.elements.btnCamera.style.background = '#f44336';
this.elements.noCamera.style.display = 'none';
this.elements.localVideo.style.display = 'block';
// Для фронтальной камеры применяем зеркальное отображение
this.elements.localVideo.style.transform =
this.currentCamera === 'front' ? 'scaleX(-1)' : 'scaleX(1)';
this.updateCameraInfo();
this.log(`✅ Камера ${this.currentCamera} запущена`, 'success');
} catch (error) {
this.log(`❌ Ошибка доступа к камере: ${error.message}`, 'error');
this.isCameraOn = false;
}
}
stopCamera() {
if (this.localStream) {
this.localStream.getTracks().forEach(track => track.stop());
this.localStream = null;
}
this.isCameraOn = false;
this.elements.btnCamera.textContent = '📷';
this.elements.btnCamera.style.background = '#4CAF50';
this.elements.localVideo.style.display = 'none';
this.elements.noCamera.style.display = 'block';
this.updateCameraInfo();
this.log('📷 Камера остановлена', 'info');
}
async switchCamera(targetCamera = null) {
if (!this.isCameraOn) {
this.log('⚠️ Сначала включите камеру', 'warning');
return;
}
// Если указан конкретный тип камеры - используем его
if (targetCamera) {
this.currentCamera = targetCamera;
} else {
// Иначе переключаем на противоположную
this.currentCamera = this.currentCamera === 'front' ? 'back' : 'front';
}
this.log(`🔄 Переключение камеры на: ${this.currentCamera}`, 'info');
this.stopCamera();
await this.startCamera();
// Обновляем все активные WebRTC соединения
await this.updateWebRTCStreams();
}
handleCameraRequest(data) {
const { sessionId, operatorId, cameraType } = data;
this.log(`📞 Запрос камеры от оператора ${operatorId}`, 'info');
this.pendingRequests.set(sessionId, { operatorId, cameraType, sessionId });
this.showRequestPanel(operatorId, sessionId);
}
handleCameraSwitchRequest(data) {
const { sessionId, cameraType } = data;
this.log(`🔄 Запрос переключения камеры: ${cameraType}`, 'info');
if (this.activeSessions.has(sessionId)) {
if (this.isCameraOn) {
// Переключаем на конкретно запрошенный тип камеры
this.switchCamera(cameraType);
} else {
// Если камера не включена, просто обновляем тип
this.currentCamera = cameraType;
this.log(`📝 Тип камеры обновлен на: ${cameraType}`, 'info');
}
}
}
handleDisconnectRequest(data) {
const { sessionId } = data;
this.log(`🔌 Запрос отключения сессии: ${sessionId}`, 'info');
if (this.activeSessions.has(sessionId)) {
this.activeSessions.delete(sessionId);
this.updateSessionCount();
}
}
showRequestPanel(operatorId, sessionId) {
this.elements.operatorId.textContent = operatorId;
this.elements.requestsPanel.classList.add('visible');
this.currentRequestSessionId = sessionId;
}
hideRequestPanel() {
this.elements.requestsPanel.classList.remove('visible');
this.currentRequestSessionId = null;
}
async acceptRequest() {
const sessionId = this.currentRequestSessionId;
const requestData = this.pendingRequests.get(sessionId);
if (!requestData) return;
this.log(`✅ Принят запрос сессии: ${sessionId}`, 'success');
// Запускаем камеру если не запущена
if (!this.isCameraOn) {
this.currentCamera = requestData.cameraType || 'back';
await this.startCamera();
}
// Добавляем сессию
this.activeSessions.set(sessionId, {
operatorId: requestData.operatorId,
cameraType: this.currentCamera,
startTime: new Date()
});
// Отправляем подтверждение с поддержкой WebRTC
this.socket.emit('camera:response', {
sessionId: sessionId,
accepted: true,
streamUrl: 'webrtc', // Указываем что используем WebRTC
cameraType: this.currentCamera
});
this.pendingRequests.delete(sessionId);
this.hideRequestPanel();
this.updateSessionCount();
}
rejectRequest() {
const sessionId = this.currentRequestSessionId;
const requestData = this.pendingRequests.get(sessionId);
if (!requestData) return;
this.log(`❌ Отклонен запрос сессии: ${sessionId}`, 'warning');
this.socket.emit('camera:response', {
sessionId: sessionId,
accepted: false,
error: 'Пользователь отклонил запрос'
});
this.pendingRequests.delete(sessionId);
this.hideRequestPanel();
}
// ===== WebRTC Methods =====
async createPeerConnection(sessionId) {
const config = {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun1.l.google.com:19302' }
]
};
const peerConnection = new RTCPeerConnection(config);
// Добавляем обработчики событий
peerConnection.onicecandidate = (event) => {
if (event.candidate) {
this.socket.emit('webrtc:ice-candidate', {
sessionId: sessionId,
candidate: event.candidate
});
this.log('🧊 ICE candidate отправлен', 'info');
}
};
peerConnection.onconnectionstatechange = () => {
this.log(`🔗 WebRTC состояние: ${peerConnection.connectionState}`, 'info');
if (peerConnection.connectionState === 'connected') {
this.log('✅ WebRTC соединение установлено', 'success');
} else if (peerConnection.connectionState === 'failed') {
this.log('❌ WebRTC соединение неудачно', 'error');
}
};
peerConnection.onicegatheringstatechange = () => {
this.log(`❄️ ICE gathering: ${peerConnection.iceGatheringState}`, 'info');
};
// Добавляем локальный поток если есть
if (this.localStream) {
this.localStream.getTracks().forEach(track => {
peerConnection.addTrack(track, this.localStream);
this.log(`🎥 Трек добавлен: ${track.kind}`, 'info');
});
}
return peerConnection;
}
async handleWebRTCOffer(data) {
const { sessionId, offer } = data;
this.log(`📞 Получен WebRTC offer для сессии: ${sessionId}`, 'info');
try {
// Создаем новое peer connection для этой сессии
const peerConnection = await this.createPeerConnection(sessionId);
this.peerConnections.set(sessionId, peerConnection);
// Устанавливаем remote description
await peerConnection.setRemoteDescription(new RTCSessionDescription(offer));
this.log('📝 Remote description установлен', 'info');
// Создаем answer
const answer = await peerConnection.createAnswer();
await peerConnection.setLocalDescription(answer);
// Отправляем answer
this.socket.emit('webrtc:answer', {
sessionId: sessionId,
answer: answer
});
this.log('✅ WebRTC answer отправлен', 'success');
} catch (error) {
this.log(`❌ Ошибка WebRTC offer: ${error.message}`, 'error');
this.socket.emit('webrtc:error', {
sessionId: sessionId,
error: error.message
});
}
}
async handleWebRTCAnswer(data) {
const { sessionId, answer } = data;
this.log(`📞 Получен WebRTC answer для сессии: ${sessionId}`, 'info');
try {
const peerConnection = this.peerConnections.get(sessionId);
if (peerConnection) {
await peerConnection.setRemoteDescription(new RTCSessionDescription(answer));
this.log('✅ WebRTC answer обработан', 'success');
}
} catch (error) {
this.log(`❌ Ошибка WebRTC answer: ${error.message}`, 'error');
}
}
async handleWebRTCIceCandidate(data) {
const { sessionId, candidate } = data;
try {
const peerConnection = this.peerConnections.get(sessionId);
if (peerConnection) {
await peerConnection.addIceCandidate(new RTCIceCandidate(candidate));
this.log('🧊 ICE candidate добавлен', 'info');
}
} catch (error) {
this.log(`❌ Ошибка ICE candidate: ${error.message}`, 'error');
}
}
async updateWebRTCStreams() {
if (!this.localStream) {
this.log('⚠️ Нет локального потока для обновления', 'warning');
return;
}
this.log('🔄 Обновление WebRTC потоков...', 'info');
// Обновляем все активные peer connections
for (const [sessionId, peerConnection] of this.peerConnections.entries()) {
try {
// Удаляем старые треки
const senders = peerConnection.getSenders();
for (const sender of senders) {
if (sender.track) {
peerConnection.removeTrack(sender);
}
}
// Добавляем новые треки
this.localStream.getTracks().forEach(track => {
peerConnection.addTrack(track, this.localStream);
});
this.log(`✅ WebRTC поток обновлен для сессии: ${sessionId}`, 'success');
} catch (error) {
this.log(`❌ Ошибка обновления WebRTC потока: ${error.message}`, 'error');
}
}
}
disconnect() {
this.log('🔌 Отключение от сервера...', 'info');
this.stopCamera();
// Закрываем все WebRTC соединения
this.peerConnections.forEach((pc, sessionId) => {
pc.close();
this.log(`🔒 WebRTC соединение закрыто: ${sessionId}`, 'info');
});
this.peerConnections.clear();
if (this.socket) {
this.socket.disconnect();
}
}
updateStatus(status) {
this.elements.status.textContent = status === 'connected' ? 'Подключено' : 'Отключено';
this.elements.status.className = `status ${status}`;
}
updateCameraInfo() {
this.elements.currentCamera.textContent =
this.isCameraOn ? this.currentCamera : 'none';
}
updateSessionCount() {
this.elements.sessionCount.textContent = this.activeSessions.size;
}
adjustLayout() {
// Адаптация под поворот экрана
setTimeout(() => {
if (this.localStream && this.elements.localVideo.srcObject) {
this.elements.localVideo.play();
}
}, 100);
}
log(message, type = 'info') {
const timestamp = new Date().toLocaleTimeString();
const logEntry = document.createElement('div');
logEntry.className = `log-entry ${type}`;
logEntry.textContent = `[${timestamp}] ${message}`;
this.elements.logs.appendChild(logEntry);
this.elements.logs.scrollTop = this.elements.logs.scrollHeight;
// Показываем логи на 3 секунды
this.elements.logs.classList.add('visible');
clearTimeout(this.logTimeout);
this.logTimeout = setTimeout(() => {
this.elements.logs.classList.remove('visible');
}, 3000);
console.log(`[Mobile] ${message}`);
}
}
// Запуск приложения
let app;
document.addEventListener('DOMContentLoaded', () => {
app = new MobileCameraApp();
});
// Предотвращение случайного обновления страницы
window.addEventListener('beforeunload', (e) => {
if (app && app.isConnected) {
e.preventDefault();
e.returnValue = '';
return '';
}
});
</script>
</body>
</html>