Files
WebDrop/web/src/core-bridge/streamClient.ts
T

49 lines
2.3 KiB
TypeScript

import { postToWorker } from './workerClient';
/**
* 页面这一侧的「边下边播」:把条目登记成一个 /__wd_stream/<id> 地址交给 <video>/<audio>,
* 播放器的每个 Range 请求由 Service Worker(public/stream-sw.js)转给这里,
* 这里把 SW 给的 MessagePort 直接交给加密线程,解密后的数据不再经过页面。
*/
interface Entry { resourceId: string; itemId: string; size: number; mime: string }
interface SwMessage { type: 'wd-stream'; id: string; range: { start?: number; end?: number; suffix?: number }; port: MessagePort }
const entries = new Map<string, Entry>();
let ready: Promise<boolean> | undefined;
function setup(): Promise<boolean> {
if (!('serviceWorker' in navigator)) return Promise.resolve(false);
return (async () => {
navigator.serviceWorker.addEventListener('message', (event: MessageEvent<SwMessage>) => {
const message = event.data;
if (message?.type !== 'wd-stream') return;
const entry = entries.get(message.id);
if (!entry) { message.port.postMessage({ error: 'gone', status: 404 }); message.port.close(); return; }
postToWorker({ operation: 'streamRange', payload: { ...entry, ...message.range } }, [message.port]);
});
await navigator.serviceWorker.register(`${import.meta.env.BASE_URL}stream-sw.js`);
await navigator.serviceWorker.ready;
if (!navigator.serviceWorker.controller) {
// 第一次安装:等 SW 接管当前页面(clients.claim)
await new Promise<void>((resolve) => {
const timer = setTimeout(resolve, 2000);
navigator.serviceWorker.addEventListener('controllerchange', () => { clearTimeout(timer); resolve(); }, { once: true });
});
}
return navigator.serviceWorker.controller !== null;
})().catch(() => false);
}
/** 登记一个可以边下边播的条目,返回播放地址;浏览器或环境不支持时返回 undefined(调用方退回整个下载) */
export async function playbackUrl(resourceId: string, itemId: string, size: number, mime: string): Promise<string | undefined> {
ready ??= setup();
if (!(await ready)) return undefined;
const id = `${crypto.randomUUID()}`;
entries.set(id, { resourceId, itemId, size, mime });
return `${import.meta.env.BASE_URL}__wd_stream/${id}`;
}
export function releasePlayback(url: string): void {
entries.delete(url.slice(url.lastIndexOf('/') + 1));
}