main funcions fixes

This commit is contained in:
2025-09-29 22:06:11 +09:00
parent 40e016e128
commit c8c3274527
7995 changed files with 1517998 additions and 1057 deletions

View File

@@ -0,0 +1,156 @@
const fs = require('fs');
const path = require('path');
const { app } = require('electron');
class ConfigManager {
constructor() {
this.configPath = path.join(app.getPath('userData'), 'config.json');
this.config = this.loadConfig();
}
loadConfig() {
try {
if (fs.existsSync(this.configPath)) {
const data = fs.readFileSync(this.configPath, 'utf8');
return JSON.parse(data);
}
} catch (error) {
console.error('Ошибка загрузки конфигурации:', error);
}
// Конфигурация по умолчанию
return {
server: {
url: 'http://localhost:3001',
autoConnect: false,
reconnectAttempts: 3,
reconnectInterval: 5000
},
ui: {
theme: 'dark',
language: 'ru',
windowBounds: {
width: 1400,
height: 900,
x: null,
y: null
},
videoFilters: {
brightness: 0,
contrast: 0,
saturation: 0,
monochrome: false,
edgeDetection: false
}
},
recording: {
quality: 'high',
format: 'webm',
maxDuration: 3600000, // 1 час в миллисекундах
autoSave: true,
saveLocation: path.join(app.getPath('videos'), 'GodEye')
}
};
}
saveConfig() {
try {
// Создаем директорию если её нет
const dir = path.dirname(this.configPath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(this.configPath, JSON.stringify(this.config, null, 2), 'utf8');
} catch (error) {
console.error('Ошибка сохранения конфигурации:', error);
}
}
get(key) {
if (!key || typeof key !== 'string') {
return this.config; // Возвращаем всю конфигурацию если ключ пустой
}
// Если передана пустая строка, возвращаем всю конфигурацию
if (key.trim() === '') {
return this.config;
}
const keys = key.split('.');
let value = this.config;
for (const k of keys) {
if (value && typeof value === 'object' && k in value) {
value = value[k];
} else {
return undefined;
}
}
return value;
}
set(key, value) {
const keys = key.split('.');
let obj = this.config;
for (let i = 0; i < keys.length - 1; i++) {
const k = keys[i];
if (!(k in obj) || typeof obj[k] !== 'object') {
obj[k] = {};
}
obj = obj[k];
}
obj[keys[keys.length - 1]] = value;
this.saveConfig();
}
// Быстрые методы для часто используемых настроек
getServerUrl() {
return this.get('server.url');
}
setServerUrl(url) {
this.set('server.url', url);
}
getAutoConnect() {
return this.get('server.autoConnect');
}
setAutoConnect(autoConnect) {
this.set('server.autoConnect', autoConnect);
}
getWindowBounds() {
return this.get('ui.windowBounds');
}
setWindowBounds(bounds) {
this.set('ui.windowBounds', bounds);
}
getVideoFilters() {
return this.get('ui.videoFilters');
}
setVideoFilters(filters) {
this.set('ui.videoFilters', filters);
}
getRecordingSettings() {
return this.get('recording');
}
ensureRecordingDirectory() {
const saveLocation = this.get('recording.saveLocation');
if (!fs.existsSync(saveLocation)) {
fs.mkdirSync(saveLocation, { recursive: true });
}
return saveLocation;
}
}
module.exports = ConfigManager;

View File

@@ -0,0 +1,110 @@
const { app, BrowserWindow, ipcMain, dialog, shell } = require('electron');
const path = require('path');
const ConfigManager = require('./config');
let mainWindow;
let config;
function createWindow() {
// Инициализируем конфигурацию
config = new ConfigManager();
// Получаем сохраненные размеры окна
const bounds = config.getWindowBounds();
mainWindow = new BrowserWindow({
width: bounds.width,
height: bounds.height,
x: bounds.x,
y: bounds.y,
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
webSecurity: false // Needed for WebRTC
},
icon: path.join(__dirname, '../assets/icon.png'),
title: 'GodEye Operator Console',
show: false,
minWidth: 1200,
minHeight: 800
});
mainWindow.loadFile('src/renderer/index.html');
// Show window when ready to prevent visual flash
mainWindow.once('ready-to-show', () => {
mainWindow.show();
// Передаем конфигурацию в renderer процесс
mainWindow.webContents.send('config-loaded', {
serverUrl: config.getServerUrl(),
autoConnect: config.getAutoConnect(),
videoFilters: config.getVideoFilters()
});
});
// Сохраняем размеры окна при изменении
mainWindow.on('resize', saveWindowBounds);
mainWindow.on('move', saveWindowBounds);
// Open DevTools in development
if (process.argv.includes('--dev')) {
mainWindow.webContents.openDevTools();
}
mainWindow.on('closed', () => {
mainWindow = null;
});
// Обработка внешних ссылок
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
shell.openExternal(url);
return { action: 'deny' };
});
}
function saveWindowBounds() {
if (mainWindow && !mainWindow.isDestroyed()) {
const bounds = mainWindow.getBounds();
config.setWindowBounds(bounds);
}
}
// IPC обработчики
ipcMain.handle('get-config', (event, key) => {
return config.get(key);
});
ipcMain.handle('set-config', (event, key, value) => {
config.set(key, value);
return true;
});
ipcMain.handle('show-save-dialog', async (event, options) => {
const result = await dialog.showSaveDialog(mainWindow, options);
return result;
});
ipcMain.handle('show-message-box', async (event, options) => {
const result = await dialog.showMessageBox(mainWindow, options);
return result;
});
app.whenReady().then(createWindow);
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
// IPC handlers for main process communication
ipcMain.handle('app-version', () => {
return app.getVersion();
});

View File

@@ -0,0 +1,902 @@
const { v4: uuidv4 } = require('uuid');
const io = require('socket.io-client');
const { ipcRenderer } = require('electron');
class GodEyeOperator {
constructor() {
this.socket = null;
this.operatorId = uuidv4();
this.currentSession = null;
this.localConnection = null;
this.remoteStream = null;
this.mediaRecorder = null;
this.recordedChunks = [];
this.isRecording = false;
this.recordingStartTime = null;
this.recordingInterval = null;
this.pingInterval = null;
this.isFullscreen = false;
this.isZoomed = false;
this.config = null;
// UI Elements
this.elements = {
connectBtn: document.getElementById('connect-btn'),
disconnectBtn: document.getElementById('disconnect-btn'),
serverUrl: document.getElementById('server-url'),
autoConnect: document.getElementById('auto-connect'),
connectionIndicator: document.getElementById('connection-indicator'),
connectionStatusText: document.getElementById('connection-status-text'),
pingIndicator: document.getElementById('ping-indicator'),
sessionInfo: document.getElementById('session-info'),
remoteVideo: document.getElementById('remoteVideo'),
videoContainer: document.querySelector('.video-container'),
videoOverlay: document.getElementById('video-overlay'),
devicesList: document.getElementById('devices-list'),
sessionsList: document.getElementById('sessions-list'),
logsContainer: document.getElementById('logs-container'),
clearLogs: document.getElementById('clear-logs'),
// Camera controls
cameraButtons: document.querySelectorAll('.camera-btn'),
// Video actions
screenshotBtn: document.getElementById('screenshot-btn'),
fullscreenBtn: document.getElementById('fullscreen-btn'),
zoomBtn: document.getElementById('zoom-btn'),
pipBtn: document.getElementById('pip-btn'),
// Image processing
brightness: document.getElementById('brightness'),
contrast: document.getElementById('contrast'),
saturation: document.getElementById('saturation'),
monochrome: document.getElementById('monochrome'),
edgeDetection: document.getElementById('edge-detection'),
resetFilters: document.getElementById('reset-filters'),
// Recording controls
startRecording: document.getElementById('start-recording'),
stopRecording: document.getElementById('stop-recording'),
playRecording: document.getElementById('play-recording'),
recordingStatus: document.getElementById('recording-status'),
recordingDuration: document.getElementById('recording-duration')
};
this.initialize();
}
async initialize() {
this.setupEventListeners();
this.updateImageFilters();
this.log('Приложение инициализировано', 'info');
// Загружаем конфигурацию
await this.loadConfig();
// Автоподключение если включено
if (this.config && this.config.autoConnect) {
setTimeout(() => this.connect(), 1000);
}
}
async loadConfig() {
try {
// Получаем конфигурацию из main процесса
ipcRenderer.on('config-loaded', (event, config) => {
this.config = config;
this.applyConfig();
});
// Запрашиваем полную конфигурацию
const fullConfig = await ipcRenderer.invoke('get-config', '');
if (fullConfig) {
this.config = {
serverUrl: await ipcRenderer.invoke('get-config', 'server.url'),
autoConnect: await ipcRenderer.invoke('get-config', 'server.autoConnect'),
videoFilters: await ipcRenderer.invoke('get-config', 'ui.videoFilters')
};
this.applyConfig();
}
} catch (error) {
this.log('Ошибка загрузки конфигурации: ' + error.message, 'error');
// Применяем настройки по умолчанию
this.config = {
serverUrl: 'http://localhost:3001',
autoConnect: false,
videoFilters: {
brightness: 0,
contrast: 0,
saturation: 0,
monochrome: false,
edgeDetection: false
}
};
this.applyConfig();
}
}
applyConfig() {
if (!this.config) return;
// Применяем настройки сервера
if (this.config.serverUrl) {
this.elements.serverUrl.value = this.config.serverUrl;
}
if (this.config.autoConnect !== undefined) {
this.elements.autoConnect.checked = this.config.autoConnect;
}
// Применяем видео фильтры
if (this.config.videoFilters) {
const filters = this.config.videoFilters;
this.elements.brightness.value = filters.brightness || 0;
this.elements.contrast.value = filters.contrast || 0;
this.elements.saturation.value = filters.saturation || 0;
this.elements.monochrome.checked = filters.monochrome || false;
this.elements.edgeDetection.checked = filters.edgeDetection || false;
this.updateImageFilters();
}
}
async saveConfig(key, value) {
try {
await ipcRenderer.invoke('set-config', key, value);
} catch (error) {
this.log('Ошибка сохранения настройки: ' + error.message, 'error');
}
}
setupEventListeners() {
// Connection controls
this.elements.connectBtn.addEventListener('click', () => this.connect());
this.elements.disconnectBtn.addEventListener('click', () => this.disconnect());
this.elements.clearLogs.addEventListener('click', () => this.clearLogs());
// Refresh devices
const refreshBtn = document.getElementById('refresh-devices');
if (refreshBtn) {
refreshBtn.addEventListener('click', () => {
this.log('Запрос обновления списка устройств...', 'info');
this.requestDevicesList();
});
}
// Auto-connect setting
this.elements.autoConnect.addEventListener('change', (e) => {
this.saveConfig('server.autoConnect', e.target.checked);
});
// Server URL changes
this.elements.serverUrl.addEventListener('change', (e) => {
this.saveConfig('server.url', e.target.value);
});
// Camera controls
this.elements.cameraButtons.forEach(btn => {
btn.addEventListener('click', () => {
const cameraType = btn.dataset.camera;
this.switchCamera(cameraType);
});
});
// Video action controls
this.elements.screenshotBtn.addEventListener('click', () => this.takeScreenshot());
this.elements.fullscreenBtn.addEventListener('click', () => this.toggleFullscreen());
this.elements.zoomBtn.addEventListener('click', () => this.toggleZoom());
this.elements.pipBtn.addEventListener('click', () => this.togglePictureInPicture());
// Image processing controls
this.elements.brightness.addEventListener('input', () => this.updateImageFilters());
this.elements.contrast.addEventListener('input', () => this.updateImageFilters());
this.elements.saturation.addEventListener('input', () => this.updateImageFilters());
this.elements.monochrome.addEventListener('change', () => this.updateImageFilters());
this.elements.edgeDetection.addEventListener('change', () => this.updateImageFilters());
this.elements.resetFilters.addEventListener('click', () => this.resetImageFilters());
// Recording controls
this.elements.startRecording.addEventListener('click', () => this.startRecording());
this.elements.stopRecording.addEventListener('click', () => this.stopRecording());
this.elements.playRecording.addEventListener('click', () => this.playLastRecording());
// Update slider values display
this.elements.brightness.addEventListener('input', (e) => {
document.getElementById('brightness-value').textContent = e.target.value;
this.saveVideoFilters();
});
this.elements.contrast.addEventListener('input', (e) => {
document.getElementById('contrast-value').textContent = e.target.value;
this.saveVideoFilters();
});
this.elements.saturation.addEventListener('input', (e) => {
document.getElementById('saturation-value').textContent = e.target.value;
this.saveVideoFilters();
});
// Save filter changes
this.elements.monochrome.addEventListener('change', () => this.saveVideoFilters());
this.elements.edgeDetection.addEventListener('change', () => this.saveVideoFilters());
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
if (e.ctrlKey || e.metaKey) {
switch (e.key) {
case 's':
e.preventDefault();
this.takeScreenshot();
break;
case 'f':
e.preventDefault();
this.toggleFullscreen();
break;
case 'r':
e.preventDefault();
if (this.isRecording) {
this.stopRecording();
} else {
this.startRecording();
}
break;
}
}
// ESC для выхода из полноэкранного режима
if (e.key === 'Escape' && this.isFullscreen) {
this.toggleFullscreen();
}
});
// Video drag for zoom mode
let isDragging = false;
let startX, startY, translateX = 0, translateY = 0;
this.elements.remoteVideo.addEventListener('mousedown', (e) => {
if (this.isZoomed) {
isDragging = true;
startX = e.clientX - translateX;
startY = e.clientY - translateY;
this.elements.remoteVideo.style.cursor = 'grabbing';
}
});
document.addEventListener('mousemove', (e) => {
if (isDragging && this.isZoomed) {
translateX = e.clientX - startX;
translateY = e.clientY - startY;
this.elements.remoteVideo.style.transform = `scale(1.5) translate(${translateX/1.5}px, ${translateY/1.5}px)`;
}
});
document.addEventListener('mouseup', () => {
if (isDragging) {
isDragging = false;
if (this.isZoomed) {
this.elements.remoteVideo.style.cursor = 'move';
}
}
});
}
saveVideoFilters() {
const filters = {
brightness: parseInt(this.elements.brightness.value),
contrast: parseInt(this.elements.contrast.value),
saturation: parseInt(this.elements.saturation.value),
monochrome: this.elements.monochrome.checked,
edgeDetection: this.elements.edgeDetection.checked
};
this.saveConfig('ui.videoFilters', filters);
}
connect() {
const serverUrl = this.elements.serverUrl.value.trim();
if (!serverUrl) {
this.log('Введите URL сервера', 'error');
return;
}
this.log(`Подключение к серверу: ${serverUrl}`, 'info');
try {
this.socket = io(serverUrl, {
transports: ['websocket', 'polling']
});
this.setupSocketListeners();
} catch (error) {
this.log(`Ошибка подключения: ${error.message}`, 'error');
}
}
setupSocketListeners() {
// Connection events
this.socket.on('connect', () => {
this.log('Подключен к серверу', 'success');
this.updateConnectionStatus(true);
this.registerOperator();
this.startPingMonitoring();
});
this.socket.on('disconnect', () => {
this.log('Отключен от сервера', 'warning');
this.updateConnectionStatus(false);
this.clearDevicesList();
this.clearSessionsList();
this.stopPingMonitoring();
});
this.socket.on('connect_error', (error) => {
this.log(`Ошибка соединения: ${error.message}`, 'error');
this.updateConnectionStatus(false);
});
// Registration events
this.socket.on('register:success', (data) => {
this.log(`Регистрация успешна: ${data.operatorId}`, 'success');
if (data.availableDevices) {
this.updateDevicesList(data.availableDevices);
} else {
this.requestDevicesList();
}
});
this.socket.on('register:error', (error) => {
this.log(`Ошибка регистрации: ${error.message}`, 'error');
});
// Devices events
this.socket.on('devices:list', (data) => {
this.updateDevicesList(data.devices);
});
// Camera events
this.socket.on('camera:response', (data) => {
this.handleCameraResponse(data);
});
this.socket.on('camera:error', (error) => {
this.log(`Ошибка камеры: ${error.message}`, 'error');
});
// WebRTC events
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.handleICECandidate(data);
});
this.socket.on('webrtc:error', (error) => {
this.log(`Ошибка WebRTC: ${error.message}`, 'error');
});
// Ping events
this.socket.on('pong', (startTime) => {
const ping = Date.now() - startTime;
this.elements.pingIndicator.textContent = `Ping: ${ping}ms`;
this.elements.pingIndicator.style.color = ping < 100 ? '#4CAF50' : ping < 300 ? '#FF9800' : '#f44336';
});
}
registerOperator() {
this.socket.emit('register:operator', {
operatorId: this.operatorId,
capabilities: ['video-stream', 'recording', 'image-processing']
});
}
requestDevicesList() {
this.socket.emit('devices:list');
}
disconnect() {
if (this.socket) {
this.socket.disconnect();
this.socket = null;
}
this.updateConnectionStatus(false);
this.log('Отключен от сервера', 'info');
}
updateConnectionStatus(connected) {
this.elements.connectBtn.disabled = connected;
this.elements.disconnectBtn.disabled = !connected;
if (connected) {
this.elements.connectionIndicator.textContent = '● Подключен';
this.elements.connectionIndicator.className = 'status connected';
this.elements.connectionStatusText.textContent = 'Подключен к серверу';
} else {
this.elements.connectionIndicator.textContent = '● Отключен';
this.elements.connectionIndicator.className = 'status disconnected';
this.elements.connectionStatusText.textContent = 'Не подключен';
this.elements.sessionInfo.textContent = '';
}
}
updateDevicesList(devices) {
const container = this.elements.devicesList;
this.log('Обновление списка устройств в UI...', 'debug');
if (!devices || devices.length === 0) {
container.innerHTML = '<div class="no-devices">Нет подключенных устройств</div>';
this.log('Нет подключенных устройств', 'warning');
return;
}
container.innerHTML = '';
devices.forEach(device => {
const deviceElement = document.createElement('div');
deviceElement.className = 'device-item';
deviceElement.innerHTML = `
<div class="device-info">
<strong>ID:</strong> ${device.deviceId}<br>
<strong>Статус:</strong> ${device.isConnected ? 'Онлайн' : 'Офлайн'}
</div>
<div class="device-capabilities">
Камеры: ${(Array.isArray(device.capabilities) ? device.capabilities.join(', ') : (device.capabilities?.cameras?.join(', ') || ''))}
</div>
<div class="device-actions">
<button class="btn-device" onclick="operator.requestCamera('${device.deviceId}', 'back')">
Подключиться
</button>
</div>
`;
container.appendChild(deviceElement);
});
this.log(`Список устройств отображён: ${devices.length} устройств`, 'success');
}
clearDevicesList() {
this.elements.devicesList.innerHTML = '<div class="no-devices">Нет подключенных устройств</div>';
}
clearSessionsList() {
this.elements.sessionsList.innerHTML = '<div class="no-sessions">Нет активных сессий</div>';
}
requestCamera(deviceId, cameraType = 'back') {
if (!this.socket || !this.socket.connected) {
this.log('Нет подключения к серверу', 'error');
return;
}
this.log(`Запрос доступа к камере ${cameraType} устройства ${deviceId}`, 'info');
this.socket.emit('camera:request', {
deviceId: deviceId,
operatorId: this.operatorId,
cameraType: cameraType
});
}
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();
} else {
this.log(`Отказ в доступе к камере: ${data.message}`, 'error');
}
}
updateCameraButtons(activeCameraType) {
this.elements.cameraButtons.forEach(btn => {
btn.classList.remove('active');
if (btn.dataset.camera === activeCameraType) {
btn.classList.add('active');
}
});
}
switchCamera(cameraType) {
if (!this.currentSession) {
this.log('Нет активной сессии для переключения камеры', 'warning');
return;
}
this.log(`Переключение на камеру: ${cameraType}`, 'info');
this.socket.emit('camera:switch', {
sessionId: this.currentSession.id,
cameraType: cameraType
});
this.updateCameraButtons(cameraType);
}
async initWebRTC() {
try {
this.localConnection = new RTCPeerConnection({
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' }
]
});
// Handle incoming stream
this.localConnection.ontrack = (event) => {
this.log('Получен видеопоток', 'success');
this.remoteStream = event.streams[0];
this.elements.remoteVideo.srcObject = this.remoteStream;
this.elements.videoOverlay.classList.add('hidden');
// Enable recording controls when stream is available
this.elements.startRecording.disabled = false;
};
// Handle ICE candidates
this.localConnection.onicecandidate = (event) => {
if (event.candidate) {
this.socket.emit('webrtc:ice-candidate', {
sessionId: this.currentSession.id,
candidate: event.candidate
});
}
};
// Create and send offer
const offer = await this.localConnection.createOffer();
await this.localConnection.setLocalDescription(offer);
this.socket.emit('webrtc:offer', {
sessionId: this.currentSession.id,
offer: offer
});
} catch (error) {
this.log(`Ошибка инициализации WebRTC: ${error.message}`, 'error');
}
}
async handleWebRTCOffer(data) {
try {
await this.localConnection.setRemoteDescription(data.offer);
const answer = await this.localConnection.createAnswer();
await this.localConnection.setLocalDescription(answer);
this.socket.emit('webrtc:answer', {
sessionId: data.sessionId,
answer: answer
});
} catch (error) {
this.log(`Ошибка обработки WebRTC offer: ${error.message}`, 'error');
}
}
async handleWebRTCAnswer(data) {
try {
await this.localConnection.setRemoteDescription(data.answer);
} catch (error) {
this.log(`Ошибка обработки WebRTC answer: ${error.message}`, 'error');
}
}
async handleICECandidate(data) {
try {
await this.localConnection.addIceCandidate(data.candidate);
} catch (error) {
this.log(`Ошибка добавления ICE candidate: ${error.message}`, 'error');
}
}
// Image Processing Methods
updateImageFilters() {
const brightness = this.elements.brightness.value;
const contrast = this.elements.contrast.value;
const saturation = this.elements.saturation.value;
const monochrome = this.elements.monochrome.checked;
const edgeDetection = this.elements.edgeDetection.checked;
let filters = [];
if (brightness !== '0') {
filters.push(`brightness(${(100 + parseInt(brightness))}%)`);
}
if (contrast !== '0') {
filters.push(`contrast(${(100 + parseInt(contrast))}%)`);
}
if (saturation !== '0') {
filters.push(`saturate(${(100 + parseInt(saturation))}%)`);
}
if (monochrome) {
filters.push('grayscale(100%)');
}
if (edgeDetection) {
// Простая имитация выделения краев через высокий контраст
filters.push('contrast(200%) brightness(50%)');
}
this.elements.remoteVideo.style.filter = filters.join(' ');
}
resetImageFilters() {
this.elements.brightness.value = 0;
this.elements.contrast.value = 0;
this.elements.saturation.value = 0;
this.elements.monochrome.checked = false;
this.elements.edgeDetection.checked = false;
// Update value displays
document.getElementById('brightness-value').textContent = '0';
document.getElementById('contrast-value').textContent = '0';
document.getElementById('saturation-value').textContent = '0';
this.updateImageFilters();
this.log('Фильтры изображения сброшены', 'info');
}
// Recording Methods
startRecording() {
if (!this.remoteStream) {
this.log('Нет видеопотока для записи', 'error');
return;
}
try {
this.recordedChunks = [];
this.mediaRecorder = new MediaRecorder(this.remoteStream, {
mimeType: 'video/webm;codecs=vp9'
});
this.mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
this.recordedChunks.push(event.data);
}
};
this.mediaRecorder.onstop = () => {
this.log('Запись остановлена', 'info');
this.elements.playRecording.disabled = false;
};
this.mediaRecorder.start(1000); // Collect data every second
this.isRecording = true;
this.recordingStartTime = Date.now();
// Update UI
this.elements.startRecording.disabled = true;
this.elements.stopRecording.disabled = false;
this.elements.recordingStatus.textContent = 'Запись...';
// Start duration counter
this.recordingInterval = setInterval(() => {
if (this.isRecording) {
const duration = Date.now() - this.recordingStartTime;
this.elements.recordingDuration.textContent = this.formatDuration(duration);
}
}, 1000);
this.log('Запись начата', 'success');
} catch (error) {
this.log(`Ошибка начала записи: ${error.message}`, 'error');
}
}
stopRecording() {
if (this.mediaRecorder && this.isRecording) {
this.mediaRecorder.stop();
this.isRecording = false;
// Clear interval
if (this.recordingInterval) {
clearInterval(this.recordingInterval);
this.recordingInterval = null;
}
// Update UI
this.elements.startRecording.disabled = false;
this.elements.stopRecording.disabled = true;
this.elements.recordingStatus.textContent = 'Готов к записи';
this.log('Запись завершена', 'success');
}
}
playLastRecording() {
if (this.recordedChunks.length === 0) {
this.log('Нет записанного видео для воспроизведения', 'warning');
return;
}
const blob = new Blob(this.recordedChunks, { type: 'video/webm' });
const url = URL.createObjectURL(blob);
// Create a new window to play the recording
const recordingWindow = window.open('', '_blank', 'width=800,height=600');
recordingWindow.document.write(`
<html>
<head>
<title>GodEye - Воспроизведение записи</title>
<style>
body { background: #000; margin: 0; display: flex; justify-content: center; align-items: center; height: 100vh; }
video { max-width: 100%; max-height: 100%; }
</style>
</head>
<body>
<video controls autoplay>
<source src="${url}" type="video/webm">
Ваш браузер не поддерживает воспроизведение видео.
</video>
</body>
</html>
`);
this.log('Воспроизведение записи в новом окне', 'info');
}
formatDuration(ms) {
const seconds = Math.floor(ms / 1000);
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
}
// Logging
log(message, type = 'info') {
const timestamp = new Date().toLocaleTimeString();
const logEntry = document.createElement('div');
logEntry.className = `log-entry ${type}`;
logEntry.innerHTML = `<span class="log-timestamp">[${timestamp}]</span>${message}`;
this.elements.logsContainer.appendChild(logEntry);
this.elements.logsContainer.scrollTop = this.elements.logsContainer.scrollHeight;
// Keep only last 100 entries
const entries = this.elements.logsContainer.children;
if (entries.length > 100) {
entries[0].remove();
}
console.log(`[${type.toUpperCase()}] ${message}`);
}
clearLogs() {
this.elements.logsContainer.innerHTML = '';
this.log('Журнал событий очищен', 'info');
}
// New video action methods
async takeScreenshot() {
if (!this.remoteStream) {
this.log('Нет активного видео потока для скриншота', 'warning');
return;
}
try {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const video = this.elements.remoteVideo;
canvas.width = video.videoWidth || video.offsetWidth;
canvas.height = video.videoHeight || video.offsetHeight;
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
// Save screenshot
const dataURL = canvas.toDataURL('image/png');
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const filename = `godeye-screenshot-${timestamp}.png`;
const saveResult = await ipcRenderer.invoke('show-save-dialog', {
title: 'Сохранить скриншот',
defaultPath: filename,
filters: [
{ name: 'PNG Images', extensions: ['png'] },
{ name: 'JPEG Images', extensions: ['jpg', 'jpeg'] },
{ name: 'All Files', extensions: ['*'] }
]
});
if (!saveResult.canceled) {
// Convert dataURL to blob and save
const base64Data = dataURL.replace(/^data:image\/png;base64,/, '');
const fs = require('fs');
fs.writeFileSync(saveResult.filePath, base64Data, 'base64');
this.log(`Скриншот сохранен: ${saveResult.filePath}`, 'success');
}
} catch (error) {
this.log(`Ошибка создания скриншота: ${error.message}`, 'error');
}
}
toggleFullscreen() {
this.isFullscreen = !this.isFullscreen;
if (this.isFullscreen) {
this.elements.videoContainer.classList.add('fullscreen');
this.elements.fullscreenBtn.textContent = '⊞ Выйти из полного экрана';
this.log('Включен полноэкранный режим', 'info');
} else {
this.elements.videoContainer.classList.remove('fullscreen');
this.elements.fullscreenBtn.textContent = '⛶ Полный экран';
this.log('Отключен полноэкранный режим', 'info');
}
}
toggleZoom() {
this.isZoomed = !this.isZoomed;
if (this.isZoomed) {
this.elements.videoContainer.classList.add('zoomed');
this.elements.zoomBtn.textContent = '🔍 Уменьшить';
this.elements.remoteVideo.style.cursor = 'move';
this.log('Включено увеличение видео', 'info');
} else {
this.elements.videoContainer.classList.remove('zoomed');
this.elements.zoomBtn.textContent = '🔍 Увеличить';
this.elements.remoteVideo.style.cursor = 'default';
this.elements.remoteVideo.style.transform = 'scale(1)';
this.log('Отключено увеличение видео', 'info');
}
}
async togglePictureInPicture() {
if (!this.remoteStream || !this.elements.remoteVideo) {
this.log('Нет активного видео потока для PiP', 'warning');
return;
}
try {
if (document.pictureInPictureElement) {
await document.exitPictureInPicture();
this.elements.pipBtn.textContent = '📺 PiP';
this.log('Выход из режима "Картинка в картинке"', 'info');
} else {
await this.elements.remoteVideo.requestPictureInPicture();
this.elements.pipBtn.textContent = '📺 Выйти из PiP';
this.log('Включен режим "Картинка в картинке"', 'info');
}
} catch (error) {
this.log(`Ошибка PiP: ${error.message}`, 'error');
}
}
startPingMonitoring() {
if (this.pingInterval) {
clearInterval(this.pingInterval);
}
this.pingInterval = setInterval(() => {
if (this.socket && this.socket.connected) {
const startTime = Date.now();
this.socket.emit('ping', startTime);
// Timeout после 5 секунд
setTimeout(() => {
if (this.elements.pingIndicator.textContent === 'Ping: ...') {
this.elements.pingIndicator.textContent = 'Ping: Timeout';
this.elements.pingIndicator.style.color = '#f44336';
}
}, 5000);
}
}, 10000); // Каждые 10 секунд
}
stopPingMonitoring() {
if (this.pingInterval) {
clearInterval(this.pingInterval);
this.pingInterval = null;
}
this.elements.pingIndicator.textContent = 'Ping: --';
this.elements.pingIndicator.style.color = '#ccc';
}
}
// Initialize application when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
window.operator = new GodEyeOperator();
});
// Expose for global access
window.GodEyeOperator = GodEyeOperator;

View File

@@ -0,0 +1,158 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GodEye Operator Console</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="app-container">
<!-- Header -->
<header class="app-header">
<div class="logo">
<h1>🔍 GodEye Operator</h1>
</div>
<div class="connection-status">
<span id="connection-indicator" class="status disconnected">● Отключен</span>
<span id="session-info"></span>
</div>
</header>
<!-- Main Content -->
<div class="main-content">
<!-- Left Panel - Video Stream -->
<div class="video-panel">
<div class="video-container">
<video id="remoteVideo" autoplay playsinline muted></video>
<div id="video-overlay" class="video-overlay">
<div id="no-stream" class="no-stream-message">
<h3>Нет видеопотока</h3>
<p>Выберите устройство и запросите доступ к камере</p>
</div>
</div>
</div>
<!-- Video Controls -->
<div class="video-controls">
<div class="control-group">
<label>Камера:</label>
<div class="camera-buttons">
<button id="btn-back" class="camera-btn" data-camera="back">Основная</button>
<button id="btn-front" class="camera-btn" data-camera="front">Фронтальная</button>
<button id="btn-wide" class="camera-btn" data-camera="wide">Широкоугольная</button>
<button id="btn-telephoto" class="camera-btn" data-camera="telephoto">Телеобъектив</button>
</div>
</div>
<div class="control-group">
<label>Действия с видео:</label>
<div class="video-actions">
<button id="screenshot-btn" class="btn-action" title="Сделать скриншот">📷 Скриншот</button>
<button id="fullscreen-btn" class="btn-action" title="Полноэкранный режим">⛶ Полный экран</button>
<button id="zoom-btn" class="btn-action" title="Увеличить">🔍 Увеличить</button>
<button id="pip-btn" class="btn-action" title="Картинка в картинке">📺 PiP</button>
</div>
</div>
</div>
<!-- Image Processing Controls -->
<div class="image-controls">
<h4>Обработка изображения</h4>
<div class="slider-group">
<label for="brightness">Яркость: <span id="brightness-value">0</span></label>
<input type="range" id="brightness" min="-100" max="100" value="0">
</div>
<div class="slider-group">
<label for="contrast">Контрастность: <span id="contrast-value">0</span></label>
<input type="range" id="contrast" min="-100" max="100" value="0">
</div>
<div class="slider-group">
<label for="saturation">Насыщенность: <span id="saturation-value">0</span></label>
<input type="range" id="saturation" min="-100" max="100" value="0">
</div>
<div class="checkbox-group">
<label>
<input type="checkbox" id="monochrome"> Монохром
</label>
<label>
<input type="checkbox" id="edge-detection"> Выделение краев
</label>
</div>
<button id="reset-filters" class="btn-secondary">Сбросить фильтры</button>
</div>
<!-- Recording Controls -->
<div class="recording-controls">
<h4>Запись</h4>
<div class="recording-buttons">
<button id="start-recording" class="btn-record">🔴 Начать запись</button>
<button id="stop-recording" class="btn-stop" disabled>⏹ Остановить</button>
<button id="play-recording" class="btn-play" disabled>▶ Воспроизвести</button>
</div>
<div class="recording-info">
<span id="recording-status">Готов к записи</span>
<span id="recording-duration">00:00</span>
</div>
</div>
</div>
<!-- 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>
<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>
<!-- 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>
<div id="devices-list" class="devices-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="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>
</div>
</div>
</div>
<!-- Scripts -->
<script src="/socket.io/socket.io.js"></script>
<script src="app.js"></script>
</body>
</html>

View File

@@ -0,0 +1,698 @@
/* Global Styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #1a1a1a;
color: #ffffff;
overflow: hidden;
}
.app-container {
display: flex;
flex-direction: column;
height: 100vh;
}
/* Header */
.app-header {
background: #2d2d2d;
padding: 10px 20px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 2px solid #333;
}
.logo h1 {
font-size: 20px;
color: #4CAF50;
}
.connection-status {
display: flex;
gap: 15px;
align-items: center;
}
.status {
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: bold;
}
.status.connected {
background: #4CAF50;
color: white;
}
.status.disconnected {
background: #f44336;
color: white;
}
#session-info {
font-size: 12px;
color: #bbb;
}
/* Main Content */
.main-content {
display: flex;
flex: 1;
overflow: hidden;
}
/* Video Panel */
.video-panel {
flex: 2;
display: flex;
flex-direction: column;
background: #1e1e1e;
border-right: 1px solid #333;
}
.video-container {
position: relative;
flex: 1;
background: #000;
display: flex;
align-items: center;
justify-content: center;
}
#remoteVideo {
width: 100%;
height: 100%;
object-fit: contain;
background: #000;
}
.video-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.8);
pointer-events: none;
}
.video-overlay.hidden {
display: none;
}
.no-stream-message {
text-align: center;
color: #888;
}
.no-stream-message h3 {
margin-bottom: 10px;
color: #aaa;
}
/* Video Controls */
.video-controls {
background: #2d2d2d;
padding: 15px;
border-top: 1px solid #333;
}
.control-group {
margin-bottom: 15px;
}
.control-group label {
display: block;
margin-bottom: 8px;
font-weight: bold;
color: #ccc;
}
.camera-buttons {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.camera-btn {
padding: 8px 16px;
border: 1px solid #555;
background: #3a3a3a;
color: #fff;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
font-size: 12px;
}
.camera-btn:hover {
background: #4a4a4a;
border-color: #777;
}
.camera-btn.active {
background: #4CAF50;
border-color: #4CAF50;
}
.camera-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Image Controls */
.image-controls {
background: #2a2a2a;
padding: 15px;
border-top: 1px solid #333;
}
.image-controls h4 {
margin-bottom: 15px;
color: #4CAF50;
font-size: 14px;
}
.slider-group {
margin-bottom: 12px;
}
.slider-group label {
display: block;
margin-bottom: 5px;
font-size: 12px;
color: #ccc;
}
.slider-group input[type="range"] {
width: 100%;
height: 4px;
background: #555;
outline: none;
border-radius: 2px;
}
.slider-group input[type="range"]::-webkit-slider-thumb {
appearance: none;
width: 16px;
height: 16px;
background: #4CAF50;
border-radius: 50%;
cursor: pointer;
}
.checkbox-group {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 15px;
}
.checkbox-group label {
display: flex;
align-items: center;
font-size: 12px;
color: #ccc;
cursor: pointer;
}
.checkbox-group input[type="checkbox"] {
margin-right: 8px;
}
/* Recording Controls */
.recording-controls {
background: #2a2a2a;
padding: 15px;
border-top: 1px solid #333;
}
.recording-controls h4 {
margin-bottom: 15px;
color: #ff5722;
font-size: 14px;
}
.recording-buttons {
display: flex;
gap: 8px;
margin-bottom: 10px;
flex-wrap: wrap;
}
.btn-record {
background: #f44336;
border: none;
color: white;
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
transition: background 0.2s;
}
.btn-record:hover:not(:disabled) {
background: #d32f2f;
}
.btn-stop {
background: #757575;
border: none;
color: white;
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
}
.btn-stop:hover:not(:disabled) {
background: #616161;
}
.btn-play {
background: #2196F3;
border: none;
color: white;
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
}
.btn-play:hover:not(:disabled) {
background: #1976D2;
}
.recording-info {
display: flex;
justify-content: space-between;
font-size: 11px;
color: #888;
}
/* Control Panel */
.control-panel {
flex: 1;
background: #252525;
display: flex;
flex-direction: column;
max-width: 350px;
}
.control-panel > div {
padding: 15px;
border-bottom: 1px solid #333;
}
/* Connection Panel */
.connection-panel h3 {
margin-bottom: 15px;
color: #4CAF50;
font-size: 14px;
}
.input-group {
margin-bottom: 10px;
}
.input-group label {
display: block;
margin-bottom: 5px;
font-size: 12px;
color: #ccc;
}
.input-group input {
width: 100%;
padding: 8px;
border: 1px solid #555;
background: #3a3a3a;
color: #fff;
border-radius: 4px;
font-size: 12px;
}
/* Buttons */
.btn-primary {
background: #4CAF50;
border: none;
color: white;
padding: 10px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
margin-right: 8px;
margin-bottom: 5px;
}
.btn-primary:hover:not(:disabled) {
background: #45a049;
}
.btn-secondary {
background: #757575;
border: none;
color: white;
padding: 10px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
margin-right: 8px;
margin-bottom: 5px;
}
.btn-secondary:hover:not(:disabled) {
background: #616161;
}
.btn-small {
padding: 6px 12px;
font-size: 11px;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Devices Panel */
.devices-panel h3,
.sessions-panel h3 {
margin-bottom: 15px;
color: #2196F3;
font-size: 14px;
}
.devices-list,
.sessions-list {
max-height: 200px;
overflow-y: auto;
}
.device-item,
.session-item {
background: #3a3a3a;
margin-bottom: 8px;
padding: 10px;
border-radius: 4px;
border-left: 3px solid #4CAF50;
}
.device-info {
font-size: 12px;
margin-bottom: 5px;
}
.device-info strong {
color: #4CAF50;
}
.device-capabilities {
font-size: 11px;
color: #888;
margin-bottom: 8px;
}
.device-actions {
display: flex;
gap: 5px;
}
.btn-device {
background: #2196F3;
border: none;
color: white;
padding: 4px 8px;
border-radius: 3px;
cursor: pointer;
font-size: 10px;
}
.btn-device:hover {
background: #1976D2;
}
.no-devices,
.no-sessions {
text-align: center;
color: #666;
font-size: 12px;
padding: 20px;
}
/* Logs Panel */
.logs-panel {
flex: 1;
display: flex;
flex-direction: column;
}
.logs-panel h3 {
margin-bottom: 15px;
color: #FF9800;
font-size: 14px;
}
.logs-container {
flex: 1;
background: #1a1a1a;
border: 1px solid #333;
border-radius: 4px;
padding: 8px;
font-family: 'Courier New', monospace;
font-size: 11px;
overflow-y: auto;
margin-bottom: 10px;
}
.log-entry {
margin-bottom: 2px;
padding: 2px 0;
}
.log-entry.info {
color: #2196F3;
}
.log-entry.success {
color: #4CAF50;
}
.log-entry.warning {
color: #FF9800;
}
.log-entry.error {
color: #f44336;
}
.log-timestamp {
color: #666;
margin-right: 8px;
}
/* Scrollbars */
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: #2a2a2a;
}
::-webkit-scrollbar-thumb {
background: #555;
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: #666;
}
/* Enhanced Button Styles */
.video-actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-top: 10px;
}
.btn-action {
padding: 8px 12px;
background: #2196F3;
border: none;
border-radius: 4px;
color: white;
cursor: pointer;
font-size: 12px;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 4px;
}
.btn-action:hover {
background: #1976D2;
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(33, 150, 243, 0.3);
}
.btn-action:active {
transform: translateY(0);
}
.btn-action:disabled {
background: #555;
cursor: not-allowed;
transform: none;
}
/* Connection Panel Enhancements */
.button-group {
display: flex;
gap: 8px;
margin-top: 10px;
}
.connection-info {
display: flex;
justify-content: space-between;
margin-top: 10px;
padding: 8px 12px;
background: #1e1e1e;
border-radius: 4px;
font-size: 12px;
border: 1px solid #333;
}
#connection-status-text {
color: #ccc;
}
#ping-indicator {
color: #4CAF50;
font-weight: bold;
}
/* Fullscreen Video Mode */
.video-container.fullscreen {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 9999;
background: #000;
}
.video-container.fullscreen #remoteVideo {
width: 100%;
height: 100%;
object-fit: contain;
}
max-height: 180px;
overflow-y: auto;
background: #181818;
border: 1px solid #333;
padding: 8px;
font-size: 12px;
line-height: 1.4;
color: #eee;
border-radius: 4px;
margin-bottom: 8px;
/* Video Zoom */
.video-container.zoomed #remoteVideo {
transform: scale(1.5);
cursor: move;
}
/* Tooltip Styles */
.tooltip {
position: relative;
}
.tooltip::after {
content: attr(title);
position: absolute;
bottom: 125%;
left: 50%;
transform: translateX(-50%);
background: #333;
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
white-space: nowrap;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s;
z-index: 1000;
}
.tooltip:hover::after {
opacity: 1;
}
/* Status Indicators */
.status-indicator {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 6px;
}
.status-indicator.online {
background: #4CAF50;
animation: pulse 2s infinite;
}
.status-indicator.offline {
background: #f44336;
}
.status-indicator.connecting {
background: #FF9800;
animation: blink 1s infinite;
}
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.5; }
100% { opacity: 1; }
}
@keyframes blink {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0; }
}
/* Responsive */
@media (max-width: 1200px) {
.main-content {
flex-direction: column;
}
.control-panel {
max-width: none;
max-height: 300px;
flex-direction: row;
overflow-x: auto;
}
.control-panel > div {
min-width: 250px;
border-right: 1px solid #333;
border-bottom: none;
}
}