// ============================================================ // DASHBOARD - Variables & State // ============================================================ var ws = null; var gsGrid = null; var isLocked = false; var state = { connected: false, cameras: null, currentCamera: null, items: null, currentItem: null, events: null, currentEvent: null, avatars: null, currentAvatar: null, system: null }; var statusInterval = null; var STORAGE_KEY = 'streamingle-dashboard-layout'; var currentTab = 'dashboard'; // ============================================================ // RETARGETING - Variables & State // ============================================================ var rtWs = null; var rtCharactersData = {}; var rtPresets = []; var rtConnected = false; var rtInitialized = false; // ============================================================ // PREVIEW - Variables & State // ============================================================ var previewImages = {}; var previewSubscribed = false; var previewFullscreenIndex = -1; var previewLastFrameTime = 0; var previewStaleCheckInterval = null; var previewDecodeStartTime = {}; // 카메라별 디코드 시작 시간 (0이면 idle, >0이면 디코딩 중) var lastWsMessageTime = 0; // 마지막 WS 메시지 수신 시각 (하트비트 감지용) var heartbeatCheckInterval = null; var clientPingInterval = null; // 클라이언트→서버 keepalive ping (iOS WiFi 절전 방지) var reconnectDelay = 300; // 재연결 딜레이 (지수 백오프) var reconnectCount = 0; // 연속 재연결 횟수 // iOS/iPadOS 감지 (Chrome on iPad도 WebKit 엔진 사용) var isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1); // ============================================================ // TAB SWITCHING // ============================================================ function switchTab(tab) { currentTab = tab; // Toggle all 3 tabs ['dashboard', 'retargeting', 'preview'].forEach(function(t) { var btnId = 'tabBtn' + t.charAt(0).toUpperCase() + t.slice(1); var contentId = 'tab' + t.charAt(0).toUpperCase() + t.slice(1); var btn = document.getElementById(btnId); var content = document.getElementById(contentId); if (btn) btn.className = 'tab-btn' + (tab === t ? ' active' : ''); if (content) content.className = 'tab-content' + (tab === t ? ' active' : ''); }); // Toggle header controls document.getElementById('dashboardControls').style.display = (tab === 'dashboard') ? '' : 'none'; document.getElementById('retargetingControls').style.display = (tab === 'retargeting') ? '' : 'none'; document.getElementById('previewControls').style.display = (tab === 'preview') ? '' : 'none'; // Preview subscribe/unsubscribe if (tab === 'preview') { previewSubscribe(); initPreviewGrid(); } else { previewUnsubscribe(); } // Lazy connect retargeting WS if (tab === 'retargeting' && !rtInitialized) { rtInitialized = true; rtConnectWebSocket(); } } // ============================================================ // DASHBOARD - Widget Definitions // ============================================================ var WIDGET_DEFS = { cameras: { title: 'Cameras', badgeId: 'camera-badge', defaultPos: { x: 0, y: 0, w: 6, h: 4, minW: 3, minH: 2 }, content: function() { return '
'; } }, items: { title: 'Items', badgeId: 'item-badge', defaultPos: { x: 6, y: 0, w: 6, h: 4, minW: 3, minH: 2 }, content: function() { return '' + '