feat: 작업실 장비 탭 밀링 카드에도 라이브 뷰

- ▶ 라이브 버튼 → go2rtc video-stream 커스텀 엘리먼트(WebRTC→MSE→MJPEG 자동폴백),
  nvr-app 라이브 모달과 동일 패턴. 릴레이 스트림 "cam"(config nvr.extraCameras)
- go2rtc Origin 검사를 통과하도록 같은 오리진 WS 프록시(/api/nvr/go2rtc-ws) 경유,
  LAN IP 하드코딩 없이 현재 오리진 기준 상대경로(https mixed-content 회피)
- 라이브 중엔 20초 스냅샷 폴링을 건너뛴다, 장비 탭을 벗어나면 연결 정리
  (stopEquipTimers → stopMillLive), 상태줄에 "● 실시간" 표시

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
kim
2026-09-24 07:52:38 +09:00
co-authored by Claude Code
parent 75b887d295
commit e01b92966a
+47 -2
View File
@@ -7,6 +7,8 @@
<link rel="icon" type="image/png" sizes="64x64" href="/homeclaw_logo.png">
<link rel="stylesheet" href="../css/styles.css">
<script>try{var _t=localStorage.getItem('homeclaw_theme')||'dark';document.documentElement.setAttribute('data-theme',_t);}catch(e){}</script>
<!-- go2rtc 라이브 플레이어(밀링 카드 ▶ 라이브용) — WebRTC→MSE→MJPEG 자동폴백 커스텀 엘리먼트 -->
<script type="module" src="/vendor/go2rtc/video-stream.js"></script>
<style>
html,body{height:100%;margin:0;padding:0;}
body{background:var(--bg);color:var(--text);font-family:system-ui,sans-serif;display:flex;flex-direction:column;overflow:hidden;height:100dvh;}
@@ -258,8 +260,13 @@ body{background:var(--bg);color:var(--text);font-family:system-ui,sans-serif;dis
<span>⚙️ CNC 밀링(프린터실)</span>
<button class="rb-equip-refresh" onclick="refreshMillWidget()">↺</button>
</div>
<div class="rb-equip-frame-wrap"><img id="mill-frame" alt="" style="display:none" onerror="millFrameError()" onload="millFrameLoaded()"><span class="rb-equip-placeholder" id="mill-placeholder">불러오는 중...</span></div>
<div class="rb-equip-status">20초마다 자동 새로고침(스냅샷) — <a href="/html/milling-app.html" target="_blank" style="color:#f59e0b">밀링 앱에서 라이브로 보기 ↗</a></div>
<div class="rb-equip-frame-wrap">
<video-stream id="mill-live" mode="webrtc,mse,mp4,mjpeg" style="display:none;width:100%;height:100%;background:#000;"></video-stream>
<img id="mill-frame" alt="" style="display:none" onerror="millFrameError()" onload="millFrameLoaded()">
<span class="rb-equip-placeholder" id="mill-placeholder">불러오는 중...</span>
<button class="rb-equip-live-btn" id="mill-live-btn" onclick="toggleMillLive()">▶ 라이브</button>
</div>
<div class="rb-equip-status">20초마다 자동 새로고침(스냅샷) — <a href="/html/milling-app.html" target="_blank" style="color:#f59e0b">밀링 앱에서 라이브로 보기 ↗</a><span id="mill-live-marker"></span></div>
</div>
</div>
</div>
@@ -576,6 +583,42 @@ function switchTab(tab){
// "장비" 탭을 보고 있을 때만 폴링한다 — 계속 켜두면 K2는 매번 짧은 WebRTC 연결을,
// 밀링은 go2rtc 프레임 요청을 반복하니 안 보는 동안은 자원을 안 쓰는 게 맞다.
const MILL_CHANNEL_ID=1000;
const MILL_GO2RTC_SRC='cam'; // go2rtc(127.0.0.1:1985) 릴레이 스트림 이름(config nvr.extraCameras.1000.go2rtcSrc)
// 밀링 카드 라이브(2026-09-24) — go2rtc video-stream 커스텀 엘리먼트(WebRTC→MSE→MJPEG
// 자동폴백)를 nvr-app 라이브 모달과 동일 패턴으로 쓴다. go2rtc(1985)에 브라우저가
// 직접 붙으면 Origin 검사로 거부되므로 같은 오리진 WS 프록시(/api/nvr/go2rtc-ws)를 거친다.
// https 페이지에서 LAN IP를 ws://로 하드코딩하면 mixed-content로 막히므로 반드시
// 현재 오리진 기준 상대경로로 만든다(K2 카메라와 동일 패턴, 09-18 발견).
const GO2RTC_WS=(location.protocol==='https:'?'wss://':'ws://')+location.host+'/api/nvr/go2rtc-ws';
let millLive=false;
function toggleMillLive(){
if(millLive)stopMillLive();else startMillLive();
}
function startMillLive(){
if(millLive)return;
const vs=document.getElementById('mill-live');
millLive=true;
const btn=document.getElementById('mill-live-btn');
btn.classList.add('on');btn.textContent='⏹ 라이브 종료';
document.getElementById('mill-frame').style.display='none';
document.getElementById('mill-placeholder').style.display='none';
vs.style.display='block';
vs.src=GO2RTC_WS+'?src='+encodeURIComponent(MILL_GO2RTC_SRC)+'&token='+encodeURIComponent(getToken());
document.getElementById('mill-live-marker').innerHTML=' · <b style="color:#ef4444">● 실시간</b>';
}
function stopMillLive(){
if(!millLive)return;
millLive=false;
const vs=document.getElementById('mill-live');
try{vs.disconnectedCallback();}catch{} // WS/WebRTC 연결 정리(라이브러리 내부 정리 로직 재사용, nvr-app closeLive와 동일)
vs.style.display='none';
const btn=document.getElementById('mill-live-btn');
btn.classList.remove('on');btn.textContent='▶ 라이브';
document.getElementById('mill-live-marker').innerHTML='';
// 스냅샷으로 즉시 복귀 — 단, 장비 탭을 벗어나며 stopEquipTimers가 부른 경우엔 안 그래도 된다.
const pane=document.getElementById('pane-equip');
if(pane&&pane.classList.contains('active'))refreshMillWidget();
}
let equipTimer=null;
function startEquipTimers(){
if(equipTimer)return;
@@ -587,9 +630,11 @@ function stopEquipTimers(){
clearInterval(equipTimer);
equipTimer=null;
stopK2Live(); // 장비 탭을 벗어나면 라이브 WebRTC 연결도 닫는다(안 보는 동안 자원 안 쓰기)
stopMillLive();
}
function refreshMillWidget(){
if(millLive)return; // 라이브 중엔 스냅샷 폴링 건너뜀 — 실시간 비디오가 이미 떠있고 간섭·낭비 방지
const img=document.getElementById('mill-frame');
img.src='/api/nvr/frame/'+MILL_CHANNEL_ID+'?t='+Date.now();
}