|
1 | | -// js/grid-manager.js |
| 1 | +// js/grid-manager.js (ФИНАЛЬНАЯ ВЕРСИЯ С ИСПРАВЛЕНИЯМИ) |
2 | 2 |
|
3 | 3 | (function(window) { |
4 | 4 | window.AppModules = window.AppModules || {}; |
|
8 | 8 | const layoutControls = document.getElementById('layout-controls'); |
9 | 9 | const MAX_GRID_SIZE = 64; |
10 | 10 | let reconnectTimers = {}; |
| 11 | + const manuallyClosedStreams = new Set(); |
11 | 12 |
|
12 | 13 | let gridCols = 2; |
13 | 14 | let gridRows = 2; |
|
42 | 43 | } |
43 | 44 |
|
44 | 45 | function initializeLayoutControls() { |
45 | | - const layouts = ["1x1", "2x2", "3x3", "4x4", "5x5", "6x6", "8x4"]; |
| 46 | + const layouts = ["1x1", "2x2", "3x3", "4x4", "5x5", "8x4","8x8"]; |
46 | 47 | layouts.forEach(layout => { |
47 | 48 | const btn = document.createElement('button'); |
48 | 49 | btn.className = 'layout-btn'; |
|
304 | 305 | try { state.player.destroy(); } catch (e) { console.error(`Error destroying JSMpeg player:`, e); } |
305 | 306 | state.player = null; |
306 | 307 | } |
307 | | - if (state.uniqueStreamIdentifier) await window.api.stopVideoStream(state.uniqueStreamIdentifier); |
| 308 | + if (state.uniqueStreamIdentifier) { |
| 309 | + await window.api.stopVideoStream(state.uniqueStreamIdentifier); |
| 310 | + } |
308 | 311 | } |
309 | 312 |
|
| 313 | + // --- ИСПРАВЛЕННАЯ ФУНКЦИЯ --- |
310 | 314 | async function stopStreamInCell(cellIndex, clearCellUI = true) { |
| 315 | + // Очищаем таймер переподключения, если он есть |
311 | 316 | if (reconnectTimers[cellIndex]) { |
312 | 317 | clearTimeout(reconnectTimers[cellIndex]); |
313 | 318 | delete reconnectTimers[cellIndex]; |
314 | 319 | } |
315 | | - |
| 320 | + |
| 321 | + // Захватываем состояние ячейки в самом начале |
316 | 322 | const state = gridCellsState[cellIndex]; |
| 323 | + |
| 324 | + // Если в ячейке ничего нет, то и делать нечего |
| 325 | + if (!state) { |
| 326 | + return; |
| 327 | + } |
| 328 | + |
| 329 | + // Безопасно получаем ID камеры и уникальный идентификатор потока |
| 330 | + const { uniqueStreamIdentifier, camera } = state; |
| 331 | + const cameraId = camera.id; |
| 332 | + |
| 333 | + // Регистрируем, что поток закрывается вручную, чтобы избежать авто-переподключения |
| 334 | + if (uniqueStreamIdentifier) { |
| 335 | + manuallyClosedStreams.add(uniqueStreamIdentifier); |
| 336 | + } |
| 337 | + |
| 338 | + // Уничтожаем плеер и останавливаем ffmpeg на бэкенде |
317 | 339 | await destroyPlayerInCell(cellIndex); |
318 | | - if (state) { |
319 | | - const isAnotherCellWithSameCam = gridCellsState.some((s, idx) => idx !== cellIndex && s?.camera.id === state.camera.id); |
320 | | - if (App.recordingStates[state.camera.id] && !isAnotherCellWithSameCam) await window.api.stopRecording(state.camera.id); |
| 340 | + |
| 341 | + // Проверяем, нужно ли остановить запись. |
| 342 | + // Это делается только если это последняя ячейка с данной камерой. |
| 343 | + const isAnotherCellWithSameCam = gridCellsState.some( |
| 344 | + (s, idx) => idx !== cellIndex && s?.camera.id === cameraId |
| 345 | + ); |
| 346 | + if (App.recordingStates[cameraId] && !isAnotherCellWithSameCam) { |
| 347 | + await window.api.stopRecording(cameraId); |
321 | 348 | } |
322 | | - gridCellsState[cellIndex] = null; |
323 | | - await App.saveConfiguration(); |
324 | | - if (clearCellUI) { |
325 | | - const cellElement = document.querySelector(`[data-cell-id='${cellIndex}']`); |
326 | | - if(cellElement) { |
327 | | - cellElement.innerHTML = `<span><i class="material-icons placeholder-icon">add_photo_alternate</i><br>${App.t('drop_camera_here')}</span>`; |
328 | | - cellElement.classList.remove('active'); |
329 | | - cellElement.draggable = false; |
| 349 | + |
| 350 | + // Финальная проверка: очищаем состояние ячейки только если оно не было изменено |
| 351 | + // другой операцией, пока мы ждали завершения `destroyPlayerInCell`. |
| 352 | + if (gridCellsState[cellIndex] === state) { |
| 353 | + gridCellsState[cellIndex] = null; |
| 354 | + |
| 355 | + if (clearCellUI) { |
| 356 | + const cellElement = document.querySelector(`[data-cell-id='${cellIndex}']`); |
| 357 | + if(cellElement) { |
| 358 | + cellElement.innerHTML = `<span><i class="material-icons placeholder-icon">add_photo_alternate</i><br>${App.t('drop_camera_here')}</span>`; |
| 359 | + cellElement.classList.remove('active'); |
| 360 | + cellElement.draggable = false; |
| 361 | + } |
330 | 362 | } |
331 | 363 | } |
| 364 | + |
| 365 | + // Сохраняем конфигурацию в любом случае |
| 366 | + await App.saveConfiguration(); |
332 | 367 | } |
333 | | - |
| 368 | + |
334 | 369 | async function toggleStream(cellIndex) { |
335 | 370 | const currentState = gridCellsState[cellIndex]; |
336 | 371 | if (!currentState || !currentState.camera) return; |
337 | | - |
| 372 | + |
| 373 | + const cellElement = document.querySelector(`[data-cell-id='${cellIndex}']`); |
| 374 | + if (!cellElement) return; |
| 375 | + |
338 | 376 | const newStreamId = currentState.streamId === 0 ? 1 : 0; |
339 | 377 | const cameraId = currentState.camera.id; |
340 | 378 | const currentVolume = currentState.player ? currentState.player.volume : 0; |
341 | | - |
| 379 | + |
| 380 | + cellElement.innerHTML = `<span>${App.t('switch_stream')}</span>`; |
| 381 | + cellElement.classList.add('active'); |
| 382 | + cellElement.draggable = true; |
| 383 | + |
342 | 384 | await destroyPlayerInCell(cellIndex); |
343 | | - const cellElement = document.querySelector(`[data-cell-id='${cellIndex}']`); |
344 | | - if(cellElement) cellElement.innerHTML = `<span>${App.t('switch_stream')}</span>`; |
345 | | - |
346 | 385 | await startStreamInCell(cellIndex, cameraId, newStreamId); |
347 | | - |
| 386 | + |
348 | 387 | const newState = gridCellsState[cellIndex]; |
349 | 388 | if (newState && newState.player) { |
| 389 | + const newCellElement = document.querySelector(`[data-cell-id='${cellIndex}']`); |
350 | 390 | newState.player.volume = currentVolume; |
351 | | - const audioBtnIcon = cellElement.querySelector('.audio-btn i'); |
352 | | - if (audioBtnIcon) audioBtnIcon.textContent = currentVolume === 0 ? 'volume_off' : 'volume_up'; |
| 391 | + const newAudioBtnIcon = newCellElement.querySelector('.audio-btn i'); |
| 392 | + if (newAudioBtnIcon) { |
| 393 | + newAudioBtnIcon.textContent = currentVolume === 0 ? 'volume_off' : 'volume_up'; |
| 394 | + } |
353 | 395 | } |
354 | 396 | } |
355 | | - |
| 397 | + |
356 | 398 | async function toggleFullscreen(cellIndex) { |
357 | 399 | const cell = document.querySelector(`[data-cell-id='${cellIndex}']`); |
358 | | - if (!cell || !gridCellsState[cellIndex]) return; |
359 | | - |
360 | 400 | const state = gridCellsState[cellIndex]; |
| 401 | + if (!cell || !state) return; |
| 402 | + |
361 | 403 | const isCurrentlyFullscreen = cell.classList.contains('fullscreen'); |
362 | | - const { id: cameraId } = state.camera; |
363 | | - const streamId = state.streamId; |
364 | | - const currentVolume = state.player ? state.player.volume : 0; |
365 | | - |
366 | | - await destroyPlayerInCell(cellIndex); |
367 | | - cell.innerHTML = `<span>${App.t('switch_fullscreen')}</span>`; |
368 | 404 |
|
369 | 405 | if (isCurrentlyFullscreen) { |
370 | | - fullscreenCellIndex = null; |
371 | 406 | gridContainer.classList.remove('fullscreen-mode'); |
372 | 407 | cell.classList.remove('fullscreen'); |
373 | | - } else { |
| 408 | + const fsBtnIcon = cell.querySelector('.fullscreen-btn i'); |
| 409 | + if(fsBtnIcon) fsBtnIcon.textContent = 'fullscreen'; |
| 410 | + fullscreenCellIndex = null; |
| 411 | + } |
| 412 | + else { |
| 413 | + if (fullscreenCellIndex !== null) { |
| 414 | + const oldFullscreenCell = document.querySelector(`[data-cell-id='${fullscreenCellIndex}']`); |
| 415 | + if(oldFullscreenCell) { |
| 416 | + oldFullscreenCell.classList.remove('fullscreen'); |
| 417 | + const oldFsBtnIcon = oldFullscreenCell.querySelector('.fullscreen-btn i'); |
| 418 | + if(oldFsBtnIcon) oldFsBtnIcon.textContent = 'fullscreen'; |
| 419 | + } |
| 420 | + } |
| 421 | + |
374 | 422 | fullscreenCellIndex = cellIndex; |
375 | 423 | gridContainer.classList.add('fullscreen-mode'); |
376 | 424 | cell.classList.add('fullscreen'); |
377 | | - } |
378 | | - |
379 | | - await startStreamInCell(cellIndex, cameraId, streamId); |
380 | | - |
381 | | - const newState = gridCellsState[cellIndex]; |
382 | | - if (newState && newState.player) { |
383 | | - newState.player.volume = currentVolume; |
384 | | - const newControls = cell.querySelector('.cell-controls'); |
385 | | - if (newControls) { |
386 | | - const audioBtnIcon = newControls.querySelector('.audio-btn i'); |
387 | | - if(audioBtnIcon) audioBtnIcon.textContent = currentVolume === 0 ? 'volume_off' : 'volume_up'; |
388 | | - const fullscreenBtnIcon = newControls.querySelector('.fullscreen-btn i'); |
389 | | - if(fullscreenBtnIcon) fullscreenBtnIcon.textContent = isCurrentlyFullscreen ? 'fullscreen' : 'fullscreen_exit'; |
390 | | - } |
| 425 | + const fsBtnIcon = cell.querySelector('.fullscreen-btn i'); |
| 426 | + if(fsBtnIcon) fsBtnIcon.textContent = 'fullscreen_exit'; |
391 | 427 | } |
392 | 428 | } |
393 | 429 |
|
394 | 430 | function handleStreamDeath(uniqueStreamIdentifier) { |
| 431 | + if (manuallyClosedStreams.has(uniqueStreamIdentifier)) { |
| 432 | + manuallyClosedStreams.delete(uniqueStreamIdentifier); |
| 433 | + console.log(`[Grid] Ignoring reconnect for manually closed stream ${uniqueStreamIdentifier}.`); |
| 434 | + return; |
| 435 | + } |
| 436 | + |
395 | 437 | const cellIndex = gridCellsState.findIndex(s => s?.uniqueStreamIdentifier === uniqueStreamIdentifier); |
396 | 438 | if (cellIndex === -1) return; |
397 | 439 |
|
398 | 440 | if (reconnectTimers[cellIndex]) { |
399 | 441 | clearTimeout(reconnectTimers[cellIndex]); |
400 | 442 | } |
401 | 443 |
|
402 | | - const { camera, streamId } = gridCellsState[cellIndex]; |
| 444 | + const state = gridCellsState[cellIndex]; |
| 445 | + if (!state) return; |
| 446 | + const { camera, streamId } = state; |
| 447 | + |
403 | 448 | const cellElement = document.querySelector(`[data-cell-id='${cellIndex}']`); |
404 | 449 | if (cellElement) { |
405 | 450 | cellElement.innerHTML = ` |
|
451 | 496 | for (let i = 0; i < gridCellsState.length; i++) { |
452 | 497 | if (gridCellsState[i]?.camera.id === cameraId) { |
453 | 498 | const oldStreamId = gridCellsState[i].streamId; |
454 | | - await destroyPlayerInCell(i); |
| 499 | + await stopStreamInCell(i, false); |
455 | 500 | await startStreamInCell(i, cameraId, oldStreamId); |
456 | 501 | } |
457 | 502 | } |
|
0 commit comments