user ea3b97b822 Add: 카메라 프리뷰 시스템 (웹 대시보드 3번째 탭)
CameraManager의 CinemachineCamera 프리셋을 라운드 로빈으로 렌더링하여
웹 대시보드에서 실시간 JPEG 프리뷰를 확인할 수 있는 시스템 추가.

- StreamDeckServerManager에 프리뷰 렌더링 로직 통합 (카메라 풀, AsyncGPUReadback, 바이너리 WebSocket)
- 대시보드 프리뷰 탭: 해상도/품질/갱신간격/그리드열 커스텀, 클릭→카메라 전환, 더블클릭→풀스크린
- 구독 기반 전송 + Page Visibility API로 불필요한 렌더링 방지

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 21:38:12 +09:00

133 lines
6.0 KiB (Stored with Git LFS)
Plaintext

<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>Streamingle Dashboard</title>
<link href="https://cdn.jsdelivr.net/npm/gridstack@10/dist/gridstack.min.css" rel="stylesheet"/>
<style>
{{CSS}}
</style>
</head>
<body>
<div class="header">
<h1>STREAMINGLE</h1>
<div class="tab-bar">
<button class="tab-btn active" id="tabBtnDashboard" onclick="switchTab('dashboard')">Dashboard</button>
<button class="tab-btn" id="tabBtnRetargeting" onclick="switchTab('retargeting')">Retargeting</button>
<button class="tab-btn" id="tabBtnPreview" onclick="switchTab('preview')">카메라 프리뷰</button>
</div>
<div class="header-controls">
<div class="connection-status" id="connectionStatus">연결 대기중...</div>
<div id="dashboardControls">
<button class="btn-icon" id="btnLock" onclick="toggleLock()" title="레이아웃 잠금/해제">&#x1F513;</button>
<button class="btn-icon" onclick="showSettings()" title="패널 설정">&#x2699;</button>
<button class="btn-icon" onclick="requestFullState()" title="새로고침">&#x1F504;</button>
</div>
<div id="retargetingControls" style="display:none;">
<button class="btn-icon" onclick="rtRefresh()" title="새로고침">&#x1F504;</button>
<button class="btn-icon" onclick="rtExpandAll()" title="모두 펼치기">&#x1F4C2;</button>
<button class="btn-icon" onclick="rtCollapseAll()" title="모두 접기">&#x1F4C1;</button>
</div>
<div id="previewControls" style="display:none;"></div>
</div>
</div>
<!-- Dashboard Tab -->
<div id="tabDashboard" class="tab-content active">
<div class="health-bar" id="healthBar">
<div class="health-item" id="health-optitrack">
<span class="health-dot offline"></span>
<span>OptiTrack</span>
</div>
<div class="health-item" id="health-facial">
<span class="health-dot offline"></span>
<span>Facial(0)</span>
</div>
<div class="health-item" id="health-recording">
<span class="health-dot offline"></span>
<span>REC</span>
</div>
<div class="health-item" id="health-remote">
<span class="health-dot offline"></span>
<span>Remote</span>
</div>
<div class="health-item" id="health-clients">
<span class="health-dot online"></span>
<span>WS(1)</span>
</div>
</div>
<div class="grid-stack" id="dashboardGrid"></div>
</div>
<!-- Retargeting Tab -->
<div id="tabRetargeting" class="tab-content">
<div class="rt-status-bar" id="rtStatusBar">
<div class="rt-connection-status" id="rtConnectionStatus">리타게팅 대기중...</div>
</div>
<div class="characters-container" id="charactersContainer">
<div class="loading-message">리타게팅 탭을 선택하면 연결됩니다.</div>
</div>
</div>
<!-- Preview Tab -->
<div id="tabPreview" class="tab-content">
<div class="preview-toolbar">
<span class="preview-info" id="previewInfo">카메라 0개</span>
<div class="preview-controls">
<label>해상도:</label>
<select id="previewResolution" onchange="updatePreviewSettings()">
<option value="240x135">240x135</option>
<option value="320x180" selected>320x180</option>
<option value="480x270">480x270</option>
</select>
<label>품질:</label>
<select id="previewQuality" onchange="updatePreviewSettings()">
<option value="30">낮음</option>
<option value="50" selected>중간</option>
<option value="75">높음</option>
</select>
<label>갱신 간격:</label>
<select id="previewRenderInterval" onchange="updatePreviewSettings()">
<option value="1">매 프레임</option>
<option value="2">2프레임</option>
<option value="3" selected>3프레임</option>
<option value="5">5프레임</option>
</select>
<label>열:</label>
<select id="previewColumns" onchange="updatePreviewColumns()">
<option value="0" selected>자동</option>
<option value="4">4</option>
<option value="5">5</option>
<option value="6">6</option>
<option value="7">7</option>
<option value="8">8</option>
</select>
</div>
</div>
<div class="preview-grid" id="previewGrid"></div>
</div>
<!-- Settings Overlay -->
<div class="settings-overlay" id="settingsOverlay" onclick="onSettingsOverlayClick(event)">
<div class="settings-panel">
<div class="settings-header">
<span>패널 설정</span>
<button class="settings-close" onclick="hideSettings()">&#x2715;</button>
</div>
<div class="settings-body" id="settingsBody"></div>
<div class="settings-footer">
<button class="btn btn-secondary btn-sm" onclick="resetLayout()">레이아웃 초기화</button>
</div>
</div>
</div>
<div class="toast" id="toast"></div>
<script src="https://cdn.jsdelivr.net/npm/gridstack@10/dist/gridstack-all.js"></script>
<script>
{{JS}}
</script>
</body>
</html>