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