108 lines
3.2 KiB
HTML
108 lines
3.2 KiB
HTML
<!doctype html>
|
|
<html lang="ko">
|
|
<meta charset="utf-8">
|
|
<title>bigbrother — WebRTC Viewer (H.264)</title>
|
|
|
|
<body>
|
|
<h3>bigbrother — WebRTC Viewer</h3>
|
|
|
|
<label>Device ID:
|
|
<input id="deviceId" placeholder="예: Q3F…(26자 Base32)" size="40">
|
|
</label>
|
|
<button id="btnConnect">Connect</button>
|
|
<button id="btnDisconnect">Disconnect</button>
|
|
<br><br>
|
|
|
|
<video id="view" playsinline autoplay muted controls style="max-width: 100%; background: #000;"></video>
|
|
<pre id="log" style="white-space:pre-wrap;"></pre>
|
|
|
|
<script>
|
|
const logEl = document.getElementById('log');
|
|
const videoEl = document.getElementById('view');
|
|
const deviceIdEl = document.getElementById('deviceId');
|
|
const signalingURL = 'wss://signaling.damhw.ee/ws/client';
|
|
const iceServers = [
|
|
{urls: 'stun:stun.cloudflare.com:3478'},
|
|
{urls: 'stun:stun.l.google.com:19302'}
|
|
];
|
|
|
|
let ws = null;
|
|
let pc = null;
|
|
let sess = null;
|
|
let remoteStream = null;
|
|
|
|
function log(...args){ logEl.textContent += args.join(' ') + '\n'; }
|
|
function sleep(ms){ return new Promise(r=>setTimeout(r,ms)); }
|
|
|
|
async function connect() {
|
|
const deviceId = deviceIdEl.value.trim();
|
|
if (!deviceId) { alert('DeviceID를 입력하세요'); return; }
|
|
localStorage.setItem('bb_device_id', deviceId);
|
|
|
|
ws = new WebSocket(signalingURL);
|
|
ws.onopen = async () => {
|
|
log('[ws] open');
|
|
ws.send(JSON.stringify({t:'connect', deviceId}));
|
|
};
|
|
ws.onmessage = async (ev) => {
|
|
const msg = JSON.parse(ev.data);
|
|
if (msg.t === 'accepted') {
|
|
sess = msg.sess;
|
|
log('[ws] accepted, sess=', sess);
|
|
|
|
pc = new RTCPeerConnection({iceServers});
|
|
// 수신 전용 비디오 트랜시버 추가하여 원격 트랙 협상
|
|
pc.addTransceiver('video', {direction: 'recvonly'});
|
|
|
|
remoteStream = new MediaStream();
|
|
videoEl.srcObject = remoteStream;
|
|
pc.ontrack = (e) => {
|
|
log('[pc] ontrack kind=', e.track.kind);
|
|
if (e.streams && e.streams[0]) {
|
|
videoEl.srcObject = e.streams[0];
|
|
} else {
|
|
remoteStream.addTrack(e.track);
|
|
}
|
|
};
|
|
|
|
pc.onicecandidate = (e) => {
|
|
if (e.candidate) {
|
|
ws && ws.send(JSON.stringify({t:'ice', sess, candidate: e.candidate}));
|
|
}
|
|
};
|
|
|
|
const offer = await pc.createOffer();
|
|
await pc.setLocalDescription(offer);
|
|
ws.send(JSON.stringify({t:'offer', sess, sdp: pc.localDescription}));
|
|
|
|
} else if (msg.t === 'answer' && msg.sess === sess) {
|
|
await pc.setRemoteDescription(msg.sdp);
|
|
log('[ws] answer set');
|
|
|
|
} else if (msg.t === 'ice' && msg.sess === sess) {
|
|
try { await pc.addIceCandidate(msg.candidate); } catch(e){ /* ignore */ }
|
|
|
|
} else if (msg.t === 'error') {
|
|
log('[ws] error:', msg.reason || '');
|
|
}
|
|
};
|
|
ws.onclose = () => log('[ws] close');
|
|
ws.onerror = (e) => log('[ws] error', e.message||'');
|
|
}
|
|
|
|
function disconnect() {
|
|
if (videoEl) { try{ videoEl.pause(); }catch{} videoEl.srcObject = null; }
|
|
if (pc){ try{ pc.close(); }catch{} pc=null; }
|
|
if (ws){ try{ ws.close(); }catch{} ws=null; }
|
|
sess = null; remoteStream = null;
|
|
log('[*] disconnected');
|
|
}
|
|
|
|
document.getElementById('btnConnect').onclick = connect;
|
|
document.getElementById('btnDisconnect').onclick = disconnect;
|
|
|
|
deviceIdEl.value = localStorage.getItem('bb_device_id') || '';
|
|
</script>
|
|
</body>
|
|
</html>
|