diff --git a/web-ui/html/k2-app.html b/web-ui/html/k2-app.html
index 318976b..de60b84 100644
--- a/web-ui/html/k2-app.html
+++ b/web-ui/html/k2-app.html
@@ -91,22 +91,33 @@ function connectCam(){
setStatus('끊김');
}
};
+ // 원래는 ev.candidate===null(ICE gathering 완전 종료)까지 기다린 뒤에만 오퍼를
+ // 보냈는데, 이 네트워크에서 그 완전종료 신호가 20~30초씩 걸림(가상/VPN 인터페이스들을
+ // 하나씩 훑고 타임아웃되는 걸로 보임) — 정작 이 프린터가 필요로 하는 STUN srflx
+ // 후보 자체는 보통 1~2초 안에 잡힘. 그래서 완전종료를 기다리지 않고 짧은 타임아웃
+ // 뒤에 그때까지 모인 후보로 바로 보낸다(2026-09-19, 20초+ 연결지연 확인 후 수정).
+ let offerSent=false;
+ function sendOfferOnce(){
+ if(offerSent || pc!==camPC) return;
+ offerSent=true;
+ fetch('/api/k2/webrtc-signal',{
+ method:'POST',
+ headers:{'Content-Type':'text/plain', ...authH()},
+ body:btoa(JSON.stringify({type:'offer',sdp:pc.localDescription.sdp})),
+ }).then(r=>r.text()).then(txt=>{
+ const res=JSON.parse(atob(txt));
+ if(res.type==='answer' && pc===camPC){
+ pc.setRemoteDescription(new RTCSessionDescription(res));
+ }
+ }).catch(()=>{ if(pc===camPC) setStatus('연결 실패'); });
+ }
pc.onicecandidate=(ev)=>{
- if(ev.candidate===null){
- fetch('/api/k2/webrtc-signal',{
- method:'POST',
- headers:{'Content-Type':'text/plain', ...authH()},
- body:btoa(JSON.stringify({type:'offer',sdp:pc.localDescription.sdp})),
- }).then(r=>r.text()).then(txt=>{
- const res=JSON.parse(atob(txt));
- if(res.type==='answer' && pc===camPC){
- pc.setRemoteDescription(new RTCSessionDescription(res));
- }
- }).catch(()=>{ if(pc===camPC) setStatus('연결 실패'); });
- }
+ if(ev.candidate===null) sendOfferOnce(); // 빨리 끝나면 그대로 씀
};
pc.addTransceiver('video',{direction:'sendrecv'});
- pc.createOffer().then(d=>pc.setLocalDescription(d)).catch(()=>{ if(pc===camPC) setStatus('연결 실패'); });
+ pc.createOffer().then(d=>pc.setLocalDescription(d)).then(()=>{
+ setTimeout(sendOfferOnce, 1500); // 안 끝나면 1.5초 뒤 그때까지 모인 후보로 진행
+ }).catch(()=>{ if(pc===camPC) setStatus('연결 실패'); });
}
function reconnectCam(){ connectCam(); }
connectCam();