79 lines
2.6 KiB
JavaScript
79 lines
2.6 KiB
JavaScript
// 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});
|
|
})());
|
|
}
|
|
});
|