This commit is contained in:
도로롱
2025-11-03 22:45:27 +09:00
parent bc60033c19
commit b6134d4b8b

View File

@@ -1,10 +1,10 @@
<!doctype html> <!doctype html>
<html lang="ko"> <html lang="ko">
<meta charset="utf-8"> <meta charset="utf-8">
<title>bigbrother — MJPEG Viewer (minimal)</title> <title>bigbrother — WebRTC Viewer (H.264)</title>
<body> <body>
<h3>bigbrother — MJPEG Viewer</h3> <h3>bigbrother — WebRTC Viewer</h3>
<label>Device ID: <label>Device ID:
<input id="deviceId" placeholder="예: Q3F…(26자 Base32)" size="40"> <input id="deviceId" placeholder="예: Q3F…(26자 Base32)" size="40">
@@ -13,12 +13,12 @@
<button id="btnDisconnect">Disconnect</button> <button id="btnDisconnect">Disconnect</button>
<br><br> <br><br>
<img id="view" alt="MJPEG stream will appear here" /> <video id="view" playsinline autoplay muted controls style="max-width: 100%; background: #000;"></video>
<pre id="log" style="white-space:pre-wrap;"></pre> <pre id="log" style="white-space:pre-wrap;"></pre>
<script> <script>
const logEl = document.getElementById('log'); const logEl = document.getElementById('log');
const imgEl = document.getElementById('view'); const videoEl = document.getElementById('view');
const deviceIdEl = document.getElementById('deviceId'); const deviceIdEl = document.getElementById('deviceId');
const signalingURL = 'wss://signaling.damhw.ee/ws/client'; const signalingURL = 'wss://signaling.damhw.ee/ws/client';
const iceServers = [ const iceServers = [
@@ -28,32 +28,17 @@ const iceServers = [
let ws = null; let ws = null;
let pc = null; let pc = null;
let dc = null;
let sess = null; let sess = null;
let key = null; let remoteStream = null;
function log(...args){ logEl.textContent += args.join(' ') + '\n'; } function log(...args){ logEl.textContent += args.join(' ') + '\n'; }
function sleep(ms){ return new Promise(r=>setTimeout(r,ms)); } function sleep(ms){ return new Promise(r=>setTimeout(r,ms)); }
async function ensureSW() {
if (!('serviceWorker' in navigator)) throw new Error('Service Worker 미지원 브라우저');
const reg = await navigator.serviceWorker.register('./sw.js', {scope: './'});
await navigator.serviceWorker.ready;
// 활성/컨트롤 보장
if (!navigator.serviceWorker.controller) {
// 새로고침 1회 필요할 수 있음. 최대한 기다려본다.
await sleep(300);
}
}
async function connect() { async function connect() {
const deviceId = deviceIdEl.value.trim(); const deviceId = deviceIdEl.value.trim();
if (!deviceId) { alert('DeviceID를 입력하세요'); return; } if (!deviceId) { alert('DeviceID를 입력하세요'); return; }
localStorage.setItem('bb_device_id', deviceId); localStorage.setItem('bb_device_id', deviceId);
await ensureSW();
key = (crypto.randomUUID && crypto.randomUUID()) || String(Date.now())+Math.random();
ws = new WebSocket(signalingURL); ws = new WebSocket(signalingURL);
ws.onopen = async () => { ws.onopen = async () => {
log('[ws] open'); log('[ws] open');
@@ -66,34 +51,17 @@ async function connect() {
log('[ws] accepted, sess=', sess); log('[ws] accepted, sess=', sess);
pc = new RTCPeerConnection({iceServers}); pc = new RTCPeerConnection({iceServers});
// 우리가 DataChannel을 먼저 만들어 SDP에 포함 // 수신 전용 비디오 트랜시버 추가하여 원격 트랙 협상
dc = pc.createDataChannel('mjpeg'); pc.addTransceiver('video', {direction: 'recvonly'});
dc.binaryType = 'arraybuffer';
dc.onopen = () => log('[dc] open'); remoteStream = new MediaStream();
dc.onclose = () => log('[dc] close'); videoEl.srcObject = remoteStream;
dc.onmessage = (e) => { pc.ontrack = (e) => {
if (typeof e.data === 'string') { log('[pc] ontrack kind=', e.track.kind);
try { if (e.streams && e.streams[0]) {
const j = JSON.parse(e.data); videoEl.srcObject = e.streams[0];
if (j.t === 'hdr') {
// 헤더 수신 → SW에 스트림 시작 알림 후 <img src> 설정
navigator.serviceWorker.controller && navigator.serviceWorker.controller.postMessage({
type:'start', key, status: j.status, headers: j.headers
});
imgEl.src = '/mjpeg?key=' + encodeURIComponent(key);
log('[dc] header received. content-type:', (j.headers && j.headers['Content-Type']) || (j.headers && j.headers['content-type']));
} else if (j.t === 'eof') {
navigator.serviceWorker.controller && navigator.serviceWorker.controller.postMessage({type:'eof', key});
log('[dc] eof');
} else if (j.t === 'error') {
log('[dc] error:', j.msg);
}
} catch(_) {}
} else { } else {
// 바이너리 청크 → SW로 전달 remoteStream.addTrack(e.track);
const chunk = e.data; // ArrayBuffer
navigator.serviceWorker.controller && navigator.serviceWorker.controller.postMessage({type:'chunk', key, chunk}, [chunk]);
} }
}; };
@@ -123,11 +91,10 @@ async function connect() {
} }
function disconnect() { function disconnect() {
if (imgEl) imgEl.src = ''; if (videoEl) { try{ videoEl.pause(); }catch{} videoEl.srcObject = null; }
if (dc){ try{ dc.close(); }catch{} dc=null; }
if (pc){ try{ pc.close(); }catch{} pc=null; } if (pc){ try{ pc.close(); }catch{} pc=null; }
if (ws){ try{ ws.close(); }catch{} ws=null; } if (ws){ try{ ws.close(); }catch{} ws=null; }
sess = null; key = null; sess = null; remoteStream = null;
log('[*] disconnected'); log('[*] disconnected');
} }