From bc60033c19869ff4fe2101b24f690f5c6f775f37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=8F=84=EB=A1=9C=EB=A1=B1?= Date: Mon, 3 Nov 2025 22:08:03 +0900 Subject: [PATCH] add trash --- public/trash/bigbrother/index.html | 140 +++++++++++++++++++++++++++++ public/trash/bigbrother/sw.js | 78 ++++++++++++++++ 2 files changed, 218 insertions(+) create mode 100644 public/trash/bigbrother/index.html create mode 100644 public/trash/bigbrother/sw.js 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

+ + + + +

+ +MJPEG stream will appear here +

+
+
+
+
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});
+    })());
+  }
+});