ext: content/ media detection (build order step 7)
Two independent signals feed one panel, per docs/05 §3: - capture/media.ts (background): webRequest-based, recognizes .m3u8/.mpd URLs and the HLS/DASH content types, deduped per (tab, url) so an HLS live-refresh doesn't re-fire. media-bridge.ts relays a hit to the tab's content script over runtime.sendMessage, and separately answers the content script's media.listVariants/media.addVariant calls by forwarding them to the background page's transport. - content/media-observer.ts: watches the page's own <video> elements (present now, added later, or with src changed) for the same extension signal, independent of what the network sniffer saw. Either firing opens content/video-panel.ts's "Download this video ▾" panel (built with createElement, matching the popup's innerHTML-free approach), which lists variants from media.listVariants and greys out any variant.drm or a wholly drmProtected manifest with "Protected content" rather than attempting it. The extension still never parses a manifest itself — that stays in the daemon, one language, one place. content/index.ts is the manifest-registered entry (content_scripts in manifest.json, added in the previous commit); build.mjs builds it as an IIFE rather than ESM, since a manifest content script has no "type": "module" declaration and an emitted top-level export would be a syntax error there. tsconfig.json adds DOM.Iterable for NodeList iteration. docs/05-extension-spec.md gets a short addendum (§8) documenting the popup/options bridge and this media-detection split, since neither was in the original design write-up. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Ed8KEmAW48v4YHdxLtqsMB
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
// Wires capture/media.ts's manifest detection to the content scripts, and answers the
|
||||
// two calls the in-page video panel needs (media.listVariants, media.addVariant). A
|
||||
// separate, simpler channel from bridge.ts's port because content scripts are per-tab
|
||||
// and use one-shot runtime.sendMessage rather than a long-lived port.
|
||||
|
||||
import type { MediaAddVariantParams, MediaListVariantsParams } from '../shared/protocol/index.js';
|
||||
import type { VeloxTransport } from './transport/index.js';
|
||||
import type { DetectedManifest } from './capture/media.js';
|
||||
|
||||
export type ContentMessage =
|
||||
| { type: 'velox-list-variants'; params: MediaListVariantsParams }
|
||||
| { type: 'velox-add-variant'; params: MediaAddVariantParams };
|
||||
|
||||
export type ManifestDetectedMessage = { type: 'velox-manifest-detected'; url: string; headers: DetectedManifest['headers'] };
|
||||
|
||||
export interface TabsLike {
|
||||
sendMessage(tabId: number, message: unknown): Promise<unknown>;
|
||||
}
|
||||
|
||||
export interface RuntimeOnMessageLike {
|
||||
addListener(cb: (msg: ContentMessage, sender: unknown, sendResponse: (r: unknown) => void) => boolean | void): void;
|
||||
}
|
||||
|
||||
/** Tells the tab's content script a manifest was seen, swallowing the "no content
|
||||
* script listening" error a tab without one (or the daemon's own tab) throws. */
|
||||
export function notifyTab(tabs: TabsLike, manifest: DetectedManifest): void {
|
||||
const message: ManifestDetectedMessage = { type: 'velox-manifest-detected', url: manifest.url, headers: manifest.headers };
|
||||
tabs.sendMessage(manifest.tabId, message).catch(() => undefined);
|
||||
}
|
||||
|
||||
export class MediaBridge {
|
||||
constructor(private readonly getTransport: () => VeloxTransport | undefined) {}
|
||||
|
||||
attach(onMessage: RuntimeOnMessageLike): void {
|
||||
onMessage.addListener((msg, _sender, sendResponse) => {
|
||||
if (msg.type !== 'velox-list-variants' && msg.type !== 'velox-add-variant') return undefined;
|
||||
void this.handle(msg).then(sendResponse);
|
||||
return true; // sendResponse is called asynchronously
|
||||
});
|
||||
}
|
||||
|
||||
private async handle(msg: ContentMessage): Promise<{ ok: true; result: unknown } | { ok: false; error: string }> {
|
||||
const t = this.getTransport();
|
||||
if (!t) return { ok: false, error: 'transport not ready' };
|
||||
try {
|
||||
const result =
|
||||
msg.type === 'velox-list-variants'
|
||||
? await t.call('media.listVariants', msg.params)
|
||||
: await t.call('media.addVariant', msg.params);
|
||||
return { ok: true, result };
|
||||
} catch (e) {
|
||||
return { ok: false, error: e instanceof Error ? e.message : String(e) };
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user