// 加密视频/音频的边下边播。 // 播放器请求 /__wd_stream/(带 Range),这里不联网:把「要哪一段」告诉发起请求的页面, // 页面把一个 MessagePort 交给加密线程,加密线程解密出这一段后一块一块从端口推回来, // 这里再把它们作为 206 响应流式交给播放器。密钥始终只在加密线程里。 const PREFIX = '/__wd_stream/'; /** 一次最多让加密线程领先播放器多少块,避免整个文件都堆在内存里 */ const WINDOW = 6; self.addEventListener('install', () => self.skipWaiting()); self.addEventListener('activate', (event) => event.waitUntil(self.clients.claim())); self.addEventListener('fetch', (event) => { const url = new URL(event.request.url); if (url.origin !== self.location.origin || !url.pathname.startsWith(PREFIX)) return; event.respondWith(serve(event).catch((error) => new Response(String(error && error.message || error), { status: 500 }))); }); function parseRange(header) { const m = /^bytes=(\d*)-(\d*)$/.exec(header || ''); if (!m) return { start: 0, end: undefined }; if (m[1] === '' && m[2] !== '') return { suffix: Number(m[2]) }; return { start: Number(m[1] || 0), end: m[2] === '' ? undefined : Number(m[2]) }; } async function serve(event) { const id = decodeURIComponent(new URL(event.request.url).pathname.slice(PREFIX.length)); const client = event.clientId ? await self.clients.get(event.clientId) : undefined; const target = client ?? (await self.clients.matchAll({ type: 'window' }))[0]; if (!target) return new Response('no client', { status: 503 }); const range = parseRange(event.request.headers.get('Range')); const channel = new MessageChannel(); const port = channel.port1; // 第一条消息:{ total, mime, start, end } 或 { error };之后是 { data } / { done } / { error } const first = new Promise((resolve) => { port.onmessage = (message) => resolve(message.data); }); target.postMessage({ type: 'wd-stream', id, range, port: channel.port2 }, [channel.port2]); const head = await Promise.race([first, new Promise((resolve) => setTimeout(() => resolve({ error: 'timeout' }), 20000))]); if (head.error) return new Response(head.error, { status: head.status || 502 }); const { total, mime, start, end } = head; let pending = []; let waiting; let finished = false; let failure; port.onmessage = (message) => { const m = message.data; if (m.data) pending.push(m.data); else if (m.done) finished = true; else if (m.error) failure = m.error; if (waiting) { const wake = waiting; waiting = undefined; wake(); } }; port.postMessage({ credit: WINDOW }); const body = new ReadableStream({ async pull(controller) { while (!pending.length && !finished && !failure) await new Promise((resolve) => { waiting = resolve; }); if (pending.length) { controller.enqueue(new Uint8Array(pending.shift())); port.postMessage({ credit: 1 }); } else if (failure) { controller.error(new Error(failure)); port.close(); } else { controller.close(); port.close(); } }, cancel() { port.postMessage({ cancel: true }); port.close(); }, }); const partial = !(start === 0 && end === total - 1 && !event.request.headers.get('Range')); return new Response(body, { status: partial ? 206 : 200, headers: { 'Content-Type': mime || 'application/octet-stream', 'Content-Length': String(end - start + 1), 'Accept-Ranges': 'bytes', 'Cache-Control': 'no-store', ...(partial ? { 'Content-Range': `bytes ${start}-${end}/${total}` } : {}), }, }); }