add trash
This commit is contained in:
140
public/trash/bigbrother/index.html
Normal file
140
public/trash/bigbrother/index.html
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ko">
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>bigbrother — MJPEG Viewer (minimal)</title>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h3>bigbrother — MJPEG 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>
|
||||||
|
|
||||||
|
<img id="view" alt="MJPEG stream will appear here" />
|
||||||
|
<pre id="log" style="white-space:pre-wrap;"></pre>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const logEl = document.getElementById('log');
|
||||||
|
const imgEl = 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 dc = null;
|
||||||
|
let sess = null;
|
||||||
|
let key = null;
|
||||||
|
|
||||||
|
function log(...args){ logEl.textContent += args.join(' ') + '\n'; }
|
||||||
|
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() {
|
||||||
|
const deviceId = deviceIdEl.value.trim();
|
||||||
|
if (!deviceId) { alert('DeviceID를 입력하세요'); return; }
|
||||||
|
localStorage.setItem('bb_device_id', deviceId);
|
||||||
|
|
||||||
|
await ensureSW();
|
||||||
|
key = (crypto.randomUUID && crypto.randomUUID()) || String(Date.now())+Math.random();
|
||||||
|
|
||||||
|
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});
|
||||||
|
// 우리가 DataChannel을 먼저 만들어 SDP에 포함
|
||||||
|
dc = pc.createDataChannel('mjpeg');
|
||||||
|
dc.binaryType = 'arraybuffer';
|
||||||
|
|
||||||
|
dc.onopen = () => log('[dc] open');
|
||||||
|
dc.onclose = () => log('[dc] close');
|
||||||
|
dc.onmessage = (e) => {
|
||||||
|
if (typeof e.data === 'string') {
|
||||||
|
try {
|
||||||
|
const j = JSON.parse(e.data);
|
||||||
|
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 {
|
||||||
|
// 바이너리 청크 → SW로 전달
|
||||||
|
const chunk = e.data; // ArrayBuffer
|
||||||
|
navigator.serviceWorker.controller && navigator.serviceWorker.controller.postMessage({type:'chunk', key, chunk}, [chunk]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (imgEl) imgEl.src = '';
|
||||||
|
if (dc){ try{ dc.close(); }catch{} dc=null; }
|
||||||
|
if (pc){ try{ pc.close(); }catch{} pc=null; }
|
||||||
|
if (ws){ try{ ws.close(); }catch{} ws=null; }
|
||||||
|
sess = null; key = null;
|
||||||
|
log('[*] disconnected');
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('btnConnect').onclick = connect;
|
||||||
|
document.getElementById('btnDisconnect').onclick = disconnect;
|
||||||
|
|
||||||
|
deviceIdEl.value = localStorage.getItem('bb_device_id') || '';
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
78
public/trash/bigbrother/sw.js
Normal file
78
public/trash/bigbrother/sw.js
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
// Minimal Service Worker to expose a streaming endpoint: /mjpeg?key=...
|
||||||
|
// Page sends {type:'start'|'chunk'|'eof', key, ...} via postMessage.
|
||||||
|
|
||||||
|
const sessions = new Map(); // key -> {headers, status, controller, queue:[], started, ended, waiter}
|
||||||
|
|
||||||
|
self.addEventListener('install', (e) => self.skipWaiting());
|
||||||
|
self.addEventListener('activate', (e) => e.waitUntil(self.clients.claim()));
|
||||||
|
|
||||||
|
self.addEventListener('message', (e) => {
|
||||||
|
const d = e.data || {};
|
||||||
|
const key = d.key || 'default';
|
||||||
|
let s = sessions.get(key);
|
||||||
|
if (!s) {
|
||||||
|
s = {headers:null, status:200, controller:null, queue:[], started:false, ended:false, waiter:null};
|
||||||
|
sessions.set(key, s);
|
||||||
|
}
|
||||||
|
if (d.type === 'start') {
|
||||||
|
s.headers = d.headers || {};
|
||||||
|
s.status = d.status || 200;
|
||||||
|
s.started = true;
|
||||||
|
if (s.waiter) { s.waiter(); s.waiter = null; }
|
||||||
|
} else if (d.type === 'chunk') {
|
||||||
|
if (s.controller) {
|
||||||
|
s.controller.enqueue(new Uint8Array(d.chunk));
|
||||||
|
} else {
|
||||||
|
s.queue.push(d.chunk);
|
||||||
|
}
|
||||||
|
} else if (d.type === 'eof') {
|
||||||
|
s.ended = true;
|
||||||
|
if (s.controller) s.controller.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('fetch', (e) => {
|
||||||
|
const url = new URL(e.request.url);
|
||||||
|
if (url.pathname === '/mjpeg') {
|
||||||
|
const key = url.searchParams.get('key') || 'default';
|
||||||
|
let s = sessions.get(key);
|
||||||
|
if (!s) {
|
||||||
|
s = {headers:null, status:200, controller:null, queue:[], started:false, ended:false, waiter:null};
|
||||||
|
sessions.set(key, s);
|
||||||
|
}
|
||||||
|
|
||||||
|
const stream = new ReadableStream({
|
||||||
|
start(controller) {
|
||||||
|
s.controller = controller;
|
||||||
|
if (s.queue.length) {
|
||||||
|
for (const q of s.queue) controller.enqueue(new Uint8Array(q));
|
||||||
|
s.queue = [];
|
||||||
|
}
|
||||||
|
if (s.ended) controller.close();
|
||||||
|
},
|
||||||
|
cancel() {
|
||||||
|
sessions.delete(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const waitHeaders = s.started ? Promise.resolve() : new Promise((resolve) => { s.waiter = resolve; setTimeout(resolve, 5000); });
|
||||||
|
|
||||||
|
e.respondWith((async () => {
|
||||||
|
await waitHeaders;
|
||||||
|
const h = new Headers();
|
||||||
|
// 받은 헤더를 그대로 복사(다중값 지원)
|
||||||
|
if (s.headers) {
|
||||||
|
for (const k in s.headers) {
|
||||||
|
const v = s.headers[k];
|
||||||
|
if (Array.isArray(v)) { v.forEach(x => h.append(k, x)); }
|
||||||
|
else if (v != null) { h.set(k, String(v)); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!h.has('Content-Type')) {
|
||||||
|
// MJPEG 기본값(필요 시 실제 헤더에서 boundary가 전달됨)
|
||||||
|
h.set('Content-Type', 'multipart/x-mixed-replace; boundary=--frame');
|
||||||
|
}
|
||||||
|
return new Response(stream, {status: s.status || 200, headers: h});
|
||||||
|
})());
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user