|
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