diff --git a/public/trash/bigbrother/index.html b/public/trash/bigbrother/index.html
new file mode 100644
index 0000000..ac06ce4
--- /dev/null
+++ b/public/trash/bigbrother/index.html
@@ -0,0 +1,140 @@
+
+
+
+
bigbrother — MJPEG Viewer (minimal)
+
+
+bigbrother — MJPEG Viewer
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/trash/bigbrother/sw.js b/public/trash/bigbrother/sw.js
new file mode 100644
index 0000000..f0bf53c
--- /dev/null
+++ b/public/trash/bigbrother/sw.js
@@ -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});
+ })());
+ }
+});