feat(web): stream video and audio while decrypting, paste images, 16px inputs on touch
This commit is contained in:
1 parent
31ae3bc39e
commit
283196938e
12 files changed
+376
-16
No files matched your search
@@ -0,0 +1,79 @@
|
||||
// 加密视频/音频的边下边播。
|
||||
// 播放器请求 /__wd_stream/<id>(带 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}` } : {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user