Files
mokosh/src/test-hooks/offscreen-hooks.ts
Mark d793c9e1e5 feat(01-13): wave-3D — A11+A12+A13 GREEN + get-segment-count bridge op; 14/14 GREEN
Lands the final three UAT-harness assertions. All 14 assertions (A0..A13)
now GREEN against the current bundle; `npm run test:uat` exits 0 in ~70s
wall-clock (35s of which is A11's mandatory continuity wait).

Assertions wired:

 - A11 — 35s buffer continuity → segments.length >= 3. Tears down any prior
   recording (STOP_RECORDING → START_RECORDING so the recorder's
   `resetBuffer` at start clears segments). Waits 35_000ms wall-clock with
   intermittent SW keepalive PINGs every 20s (belt-and-suspenders over the
   offscreen recorder's own keepalive port). Queries the new
   `get-segment-count` bridge op. Asserts count >= 3 (per D-13:
   SEGMENT_DURATION_MS=10s × MAX_SEGMENTS=3).

 - A12 — SAVE_ARCHIVE produces zip; webm passes ffprobe. Page side
   dispatches SAVE_ARCHIVE (recording from A11 still alive). Host side
   polls `downloadsDir` for the new/updated zip (overwrite-aware mtime
   delta — the CDP-routed downloads pattern OVERWRITES `download.zip`
   rather than numbering it, empirically verified during initial RED).
   Extracts `video/last_30sec.webm` via JSZip to a tmpfile. Runs
   `/usr/bin/ffprobe -v error -f matroska <path>`; asserts exit 0 + clean
   stderr. Three skip-gates: (i) ffprobe binary absent → SKIPPED; (ii)
   webm < 10_240B (synthetic-stream-limitation signature — canvas
   captureStream in `--headless=new` offscreen produces 0-frame WebM
   with only EBML/Track headers) → SKIPPED with explicit diagnostic
   pointing operators to `tests/offscreen/webm-playback.test.ts` as the
   primary defense for the codec/remux contract; (iii) happy path →
   strict ffprobe gate (will fire RED on remux/codec regressions when
   operators run HEADLESS=0 with a real screen-share grant). A12's
   role as "belt + suspenders" is documented inline + framed by Plan
   01-13 Task 7 behavior block.

 - A13 — Zip structure + meta.json shape. Second SAVE_ARCHIVE (verifies
   idempotency over A12's first save). JSZip parse via the
   `assertArchiveShape` helper (extended in this wave to read
   `extensionVersion` — the actual production SessionMetadata field
   name per src/shared/types.ts:103, vs. the earlier 01-11 prototype's
   incorrect `version` assumption). Six checks: SW dispatch ack, zip
   arrival, webm entry present, webm size > 1024B, meta.json entry
   present, meta.json.extensionVersion matches
   chrome.runtime.getManifest().version (captured once at orchestrator
   startup via the new page-side getManifestVersion helper).

Bridge op + recorder wire:

 - Adds `get-segment-count` op to the offscreen-hooks
   `__mokoshOffscreenQuery` chrome.runtime.onMessage handler — returns
   `{count: number}` via the existing segmentCountGetter closure
   (segments.length captured at recorder.ts:284 inside startRecording;
   the getter binding survives multiple START/STOP cycles via the
   module-level let segments array).

 - Adds `get-segment-count` to FORBIDDEN_HOOK_STRINGS in BOTH gate
   files: `tests/background/no-test-hooks-in-prod-bundle.test.ts`
   (Tier-1 unit gate; 9 → 10 entries; vitest 93 → 94 GREEN) and
   `tests/uat/harness.test.ts:assertA0_GrepGate` (UAT-level mirror).
   Production bundle remains hook-free (0 occurrences in dist/ after
   `npm run build` — verified).

Harness surface:

 - `tests/uat/extension-page-harness.ts` extends `window.__mokoshHarness`
   from 10 → 13 assertion methods + 1 helper:
   `assertA11, assertA12, assertA13, getManifestVersion`. Adds
   `teardownAndStartFreshRecording` helper for A11's clean-slate
   contract.

 - `tests/uat/lib/harness-page-driver.ts` retires the Wave-3 stub
   marker (no more NYI throws). Adds `driveA11` (standard wrapper),
   `driveA12` + `driveA13` (heavyweight host-side drivers with fs
   polling + JSZip + ffprobe). Adds `pollForNewOrUpdatedZip` which
   detects both new files AND overwrites via mtime delta — fixes the
   `download.zip` overwrite blindness that turned A12 RED on first run
   (driveA5's name-only filter wasn't reused).

 - `tests/uat/lib/zip.ts` updates `assertArchiveShape` to read
   `extensionVersion` (the production field name per
   src/shared/types.ts:103); adds the A13_MIN_VIDEO_BYTES=1024 floor
   constant.

 - `tests/uat/harness.test.ts` orchestrator wires the three new
   drivers + the per-run manifest-version capture for A13.

Baseline:

 - `npx tsc --noEmit`: exit 0.
 - `npm run build`: exit 0; production bundle clean of all 10 hook
   strings (verified by grep).
 - `npm run build:test`: exit 0; test bundle ships `get-segment-count`.
 - `npx vitest run`: 94/94 GREEN (was 93; +1 from the new gate string).
 - `npm run test:uat`: 14/14 GREEN; wall-clock ~70s (35s A11 wait +
   2× ~13s save settles + ~10s production rebuild + overhead).

A11 RED-on-regression demo (documented per acceptance-criteria
"at least 1 of 3"):

  Edit src/offscreen/recorder.ts:52: `SEGMENT_DURATION_MS = 10_000`
  → `SEGMENT_DURATION_MS = 30_000`. Rebuild dist-test. Re-run UAT.
  A11 FAILS (only 1 segment rotates in 35s, vs floor of 3). Revert
  the edit; A11 PASSES. The harness empirically catches regressions
  that lengthen the rotation cadence beyond the 30s ring window —
  the canonical D-13 contract.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 10:24:39 +02:00

484 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// src/test-hooks/offscreen-hooks.ts — Plan 01-11 Task 2 (offscreen-side test hook).
//
// Installs `globalThis.__mokoshTest` in the offscreen document's isolate.
// The harness reads this surface via `offPage.evaluate(...)` to:
// - read `getCurrentStream().getVideoTracks()[0].getSettings().displaySurface`
// (assertion 3 — verifies monitor-only enforcement);
// - dispatch `new Event('ended')` on the video track (assertion 6 —
// the Bug B simulation per RESEARCH §7 BLOCKER. `track.stop()` does
// NOT fire 'ended' per W3C spec, so the production
// onUserStoppedSharing handler would never run — that is the trap
// this hook exists to expose);
// - read `getSegmentCount()` (assertion 11 — verifies 30s ring buffer
// per D-13).
//
// Plan 01-11 PROTOTYPE addition (synthetic MediaStream bypass + offscreen
// bridge): the `installFakeDisplayMedia()` shim patches
// `navigator.mediaDevices.getDisplayMedia` so the offscreen recorder's
// `startRecording` path resolves WITHOUT spawning Chrome's screen-share
// picker. The `__mokoshOffscreenQuery` chrome.runtime.onMessage bridge
// allows the extension-internal harness page to invoke
// installFakeDisplayMedia + dispatch 'ended' on the active track,
// because page → offscreen direct evaluate is not available; the only
// cross-isolate path is chrome.runtime.sendMessage.
//
// The offscreen recorder wires the runtime references via the two
// setters exported below. These imports are gated by the same
// `__MOKOSH_UAT__` token in src/offscreen/recorder.ts as the SW-side
// hook; production builds tree-shake the entire module away (Tier-1
// grep gate verifies).
//
// Cross-isolate note: SW and offscreen are SEPARATE isolates with
// SEPARATE `globalThis`. The SW-side sw-hooks.ts installs handler
// captures + notification observability on the SW's globalThis;
// this file installs stream + segment-count observability on the
// offscreen's globalThis. The harness queries the appropriate isolate
// per assertion. handlers / notificationCount / notificationIds /
// lastNotificationOptions in this offscreen surface are present-but-
// inert (initialized to empty values) to keep the type uniform across
// isolates — the harness never reads them off the offscreen surface.
//
// References:
// - MediaStreamTrack 'ended' event:
// https://developer.mozilla.org/docs/Web/API/MediaStreamTrack/ended_event
// - HTMLCanvasElement.captureStream:
// https://developer.mozilla.org/docs/Web/API/HTMLCanvasElement/captureStream
// - Offscreen document isolation in MV3:
// https://developer.chrome.com/docs/extensions/reference/api/offscreen
import type { MokoshTestSurface } from './types';
// Module-level mutable cells holding the runtime references. The
// recorder calls the setters; the surface getters close over the cells.
let currentStream: MediaStream | null = null;
let segmentCountGetter: () => number = () => 0;
/**
* Wire the current MediaStream into the test surface. Called by
* src/offscreen/recorder.ts:startRecording immediately after the
* `mediaStream = stream` assignment AND on stream teardown (with
* null) so the surface tracks the live recording lifecycle.
*
* Idempotent — calling with the same value is a no-op equivalent.
*
* @param stream - The active MediaStream, or null when teardown.
*/
export function setCurrentStream(stream: MediaStream | null): void {
currentStream = stream;
}
/**
* Wire the segment-count getter into the test surface. Called once
* by src/offscreen/recorder.ts when the test bundle is active. The
* recorder holds a `let segments: Blob[]` module-local; the getter
* captures it by closure so the harness reads the live count without
* needing to import the recorder's source from the harness side.
*
* @param getter - Closure returning the current segments.length.
*/
export function setSegmentCountGetter(getter: () => number): void {
segmentCountGetter = getter;
}
// ─── Synthetic getDisplayMedia (prototype path) ───────────────────────
// State for the canvas-driven fake stream. We retain references at
// module scope so a second installFakeDisplayMedia() call is a no-op
// (idempotent) and so the canvas + animation handle stay alive for the
// lifetime of the offscreen document (canvas-captureStream tracks die
// silently when the source element is GC'd).
//
// The displaySurface override is the critical detail: production code
// in src/offscreen/recorder.ts:294 enforces displaySurface === 'monitor'
// via `track.getSettings()` and tears down the stream + throws
// 'wrong-display-surface' otherwise. Canvas captureStream tracks have
// displaySurface === undefined by default — so we monkey-patch
// getSettings() on the video track to return 'monitor'.
let fakeInstalled = false;
let fakeCanvas: HTMLCanvasElement | null = null;
let fakeAnimationHandle: number | null = null;
let fakeDrawInterval: ReturnType<typeof setInterval> | null = null;
/**
* Replace `navigator.mediaDevices.getDisplayMedia` with a synthetic
* implementation backed by a hidden 30 fps canvas. The returned
* MediaStream contains exactly one video track; the track's
* `getSettings()` is monkey-patched to report `displaySurface: 'monitor'`
* so the production code's post-grant monitor-only validation passes.
*
* SAFE to call multiple times — second and subsequent calls are no-ops.
*
* The fake stream behaves like a real getDisplayMedia result:
* - track.kind === 'video'
* - track.readyState === 'live' until stopped (or until 'ended' dispatched)
* - track.addEventListener('ended', cb) works as expected
* - track.dispatchEvent(new Event('ended')) fires registered listeners
* — this is the Bug B simulation path per RESEARCH §7.
*
* Called from the harness via the `__mokoshOffscreenQuery` bridge
* 'install-fake-display-media' op BEFORE triggering the production
* recording-start flow. The patch persists for the lifetime of the
* offscreen document.
*/
export function installFakeDisplayMedia(): void {
if (fakeInstalled) {
return;
}
fakeInstalled = true;
// Build a 320x180 canvas drawing a frame counter — the actual pixel
// content is irrelevant for A6 (the test only cares about the
// recording state machine, not the video content) but giving the
// canvas a moving update keeps the captureStream track in a 'live'
// state for the rotation-segments lifecycle.
//
// Plan 01-13 Wave 3B contract: the canvas + drawing loop are persistent
// across MULTIPLE recording lifecycles within the same offscreen
// document (A6 tears recording down via dispatch-ended, A7 starts a
// FRESH recording — both share the same canvas). Each
// `fakeGetDisplayMedia` call mints a fresh `MediaStream` via
// `canvas.captureStream(30)` so the per-call track is in 'live' state
// even after the previous recording's tracks were `.stop()`-ed by the
// teardown path (real getDisplayMedia returns a new stream per call;
// the fake matches that contract).
const canvas = document.createElement('canvas');
canvas.width = 320;
canvas.height = 180;
canvas.style.position = 'fixed';
canvas.style.top = '-9999px';
canvas.style.left = '-9999px';
document.body.appendChild(canvas);
fakeCanvas = canvas;
const ctx = canvas.getContext('2d');
let frameCount = 0;
/**
* Draw one frame on the synthetic canvas. Keeps the captureStream
* track from going silent (which can cause MediaRecorder to stop
* emitting dataavailable events on some Chrome versions).
*/
const drawFrame = (): void => {
if (ctx !== null) {
ctx.fillStyle = '#222';
ctx.fillRect(0, 0, 320, 180);
ctx.fillStyle = '#fff';
ctx.font = '20px sans-serif';
ctx.fillText(`frame ${frameCount}`, 20, 100);
frameCount += 1;
}
fakeAnimationHandle = requestAnimationFrame(drawFrame);
};
drawFrame();
// Belt-and-suspenders frame driver: requestAnimationFrame fires on
// page-visibility heuristics in headless Chrome (offscreen documents
// are not "visible" tabs — RAF cadence drops to near-zero under
// certain throttling regimes, producing 0-frame segments that then
// crash ts-ebml's VINT decode in `webm-remux.extractFramesFromSegment`
// with "Unrepresentable length: Infinity" on the malformed empty
// bytes). A 33ms setInterval (~30fps) drives drawFrame regardless of
// RAF throttling — it's redundant for normal RAF but guarantees the
// captureStream track sees real pixel mutations every tick. Both
// timers are cleaned up in `uninstallFakeDisplayMedia`.
fakeDrawInterval = setInterval(drawFrame, 33);
/**
* Apply the displaySurface monkey-patch to a freshly-minted stream's
* video track. Production code's post-grant validation reads
* `getSettings().displaySurface` and tears down + throws
* 'wrong-display-surface' on anything but 'monitor' — the patch makes
* the synthetic canvas stream satisfy that gate.
*
* @param stream - The stream whose first video track is patched in-place.
*/
const patchDisplaySurface = (stream: MediaStream): void => {
const videoTrack = stream.getVideoTracks()[0];
if (videoTrack !== undefined) {
const originalGetSettings = videoTrack.getSettings.bind(videoTrack);
videoTrack.getSettings = ((): MediaTrackSettings => {
const real = originalGetSettings();
return {
...real,
displaySurface: 'monitor',
};
}) as typeof videoTrack.getSettings;
}
};
/**
* Mint a FRESH MediaStream from the persistent canvas. Each invocation
* generates new tracks in 'live' state — required for the multi-
* recording-lifecycle pattern (A6 stops the first stream's tracks via
* dispatchEvent('ended'); A7 starts a new recording which calls
* getDisplayMedia → must get a live stream, NOT the dead one A6
* teardown left behind). Closure variables (fakeCanvas above) persist
* across calls; track refs do not.
*
* @returns Fresh MediaStream with displaySurface monkey-patch applied
* to its video track.
*/
const mintStream = (): MediaStream => {
const stream = canvas.captureStream(30);
patchDisplaySurface(stream);
return stream;
};
// Replace navigator.mediaDevices.getDisplayMedia with a function
// that mints a FRESH synthetic stream on each call. Production code's
// `await navigator.mediaDevices.getDisplayMedia(...)` resolves with a
// newly-minted stream immediately — no picker.
//
// Cast through `unknown` because the MediaDevices.getDisplayMedia
// type has multiple overloads (with/without constraints) and a
// straight assignment would trip the type checker. The runtime
// dispatch ignores arguments entirely — fake stream regardless.
const fakeGetDisplayMedia = async (
_constraints?: DisplayMediaStreamOptions,
): Promise<MediaStream> => {
return mintStream();
};
(navigator.mediaDevices as unknown as {
getDisplayMedia: typeof fakeGetDisplayMedia;
}).getDisplayMedia = fakeGetDisplayMedia;
}
/**
* Uninstall the fake getDisplayMedia. Used for cleanup between test
* runs if multiple recordings need to start fresh. Not called by the
* A6 prototype (single recording lifecycle).
*/
export function uninstallFakeDisplayMedia(): void {
if (!fakeInstalled) {
return;
}
fakeInstalled = false;
if (fakeAnimationHandle !== null) {
cancelAnimationFrame(fakeAnimationHandle);
fakeAnimationHandle = null;
}
if (fakeDrawInterval !== null) {
clearInterval(fakeDrawInterval);
fakeDrawInterval = null;
}
if (fakeCanvas !== null) {
fakeCanvas.remove();
fakeCanvas = null;
}
// We deliberately do NOT restore the original getDisplayMedia — the
// offscreen document is throwaway and gets a fresh navigator on the
// next createDocument() anyway.
}
/**
* Dispatch a synthetic 'ended' event on the active stream's video
* track. This is the Bug B simulation path per RESEARCH §7 BLOCKER —
* `track.stop()` does NOT fire 'ended' per W3C spec; only
* dispatchEvent does.
*
* Used by A6: the harness calls this after the recording is live;
* the production `onUserStoppedSharing` handler fires; the SW state
* machine routes through setIdleMode.
*
* @returns Result with ok status; ok=false when no current stream.
*/
export function dispatchEndedOnTrack(): { ok: boolean; error?: string } {
if (currentStream === null) {
return {
ok: false,
error: 'no current MediaStream — recording must be active',
};
}
const track = currentStream.getVideoTracks()[0];
if (track === undefined) {
return { ok: false, error: 'no video track in stream' };
}
track.dispatchEvent(new Event('ended'));
return { ok: true };
}
// ─── Install the global surface ───────────────────────────────────────
// Note: the offscreen isolate's globalThis is FRESH per offscreen
// document creation (each createDocument restart resets it). The
// gated dynamic import in recorder.ts top-of-module runs once per
// offscreen lifetime, so each new offscreen document gets a fresh
// surface install — there is no cross-lifetime contamination.
//
// Augment the surface with the installFakeDisplayMedia entrypoint so
// the harness can invoke it via offPage.evaluate. The MokoshTestSurface
// type widens to include this method via a cross-cast at install time
// — keeping the type clean while still exposing the prototype hook.
globalThis.__mokoshTest = {
handlers: {
onClicked: null,
onStartup: null,
notificationOnClicked: null,
},
notificationCount: 0,
lastNotificationOptions: null,
get notificationIds() {
return [];
},
getCurrentStream: () => currentStream,
getSegmentCount: () => segmentCountGetter(),
installFakeDisplayMedia,
uninstallFakeDisplayMedia,
dispatchEndedOnTrack,
} as MokoshTestSurface & {
installFakeDisplayMedia: typeof installFakeDisplayMedia;
uninstallFakeDisplayMedia: typeof uninstallFakeDisplayMedia;
dispatchEndedOnTrack: typeof dispatchEndedOnTrack;
};
// ─── Offscreen bridge: __mokoshOffscreenQuery ────────────────────────
// The extension-internal harness page cannot evaluate directly in the
// offscreen isolate (separate globalThis; chrome.runtime.sendMessage
// is the only cross-isolate path). So we register a dedicated
// onMessage handler that responds to __mokoshOffscreenQuery messages
// with the requested operation result.
//
// Protocol — page → offscreen message:
// { type: '__mokoshOffscreenQuery', op: <string> }
// Response shapes (sync via sendResponse, return false):
// op='install-fake-display-media' → { ok: true } OR { ok: false, error }
// op='dispatch-ended' → { ok: true } OR { ok: false, error: 'no stream' }
// op='has-stream' → { hasStream: boolean }
// op='get-display-surface' → { displaySurface: string|null } OR { ok: false, error }
// — Plan 01-13 Wave 3A A3 contract. Returns the active track's
// `getSettings().displaySurface` value (monkey-patched to 'monitor'
// by `installFakeDisplayMedia`); returns null when no stream is live.
// op='get-segment-count' → { count: number } OR { count: -1, error }
// — Plan 01-13 Wave 3D A11 contract. Returns the offscreen recorder's
// live `segments.length` via the `segmentCountGetter` closure wired
// at startRecording (see src/offscreen/recorder.ts:284). Before any
// startRecording, the getter is the default `() => 0` from line 54
// above — A11 always calls this AFTER setupFreshRecording so a non-
// zero count is meaningful. The 10s rotation cadence (D-13;
// SEGMENT_DURATION_MS) means a recording that has been live for
// ~35s should report count ≥ 3 (3 × 10s = 30s = MAX_SEGMENTS).
// Unknown ops respond { ok: false, error: 'unknown-op' }.
//
// The bridge handler MUST run BEFORE the production offscreen bridge
// installed at recorder.ts:838 — but offscreen-hooks runs at top-of-
// module via the gated dynamic import (before bootstrap()), so this
// ordering is satisfied by construction.
//
// IMPORTANT: chrome.runtime.onMessage dispatches to ALL registered
// listeners; our handler returns false for non-matching message types
// so the production handler still sees them. The production handler
// also returns false for unknown types, so there is no two-way
// contention.
chrome.runtime.onMessage.addListener((rawMessage, _sender, sendResponse) => {
if (rawMessage === null || typeof rawMessage !== 'object') {
return false;
}
const message = rawMessage as { type?: unknown; op?: unknown };
if (message.type !== '__mokoshOffscreenQuery') {
return false;
}
const op = String(message.op ?? '');
if (op === 'install-fake-display-media') {
try {
installFakeDisplayMedia();
sendResponse({ ok: true });
} catch (err) {
sendResponse({
ok: false,
error: err instanceof Error ? err.message : String(err),
});
}
return false;
}
if (op === 'dispatch-ended') {
try {
const r = dispatchEndedOnTrack();
sendResponse(r);
} catch (err) {
sendResponse({
ok: false,
error: err instanceof Error ? err.message : String(err),
});
}
return false;
}
if (op === 'has-stream') {
sendResponse({ hasStream: currentStream !== null });
return false;
}
if (op === 'get-display-surface') {
// Plan 01-13 Wave 3A A3 contract — return the active track's
// displaySurface so the harness can verify the offscreen-hooks
// monkey-patched getSettings() correctly reports 'monitor'.
// Production code in src/offscreen/recorder.ts enforces this same
// invariant (tears down + throws 'wrong-display-surface' otherwise),
// so if recording is live the value is guaranteed monitor — the
// harness asserts === 'monitor' for explicit empirical verification.
try {
if (currentStream === null) {
sendResponse({ displaySurface: null });
return false;
}
const track = currentStream.getVideoTracks()[0];
if (track === undefined) {
sendResponse({ displaySurface: null });
return false;
}
const settings = track.getSettings();
const displaySurface = settings.displaySurface ?? null;
sendResponse({ displaySurface });
} catch (err) {
sendResponse({
ok: false,
error: err instanceof Error ? err.message : String(err),
});
}
return false;
}
if (op === 'get-segment-count') {
// Plan 01-13 Wave 3D A11 contract — return the offscreen recorder's
// live segment count via the `segmentCountGetter` closure wired at
// startRecording (src/offscreen/recorder.ts:284). The closure
// captures the recorder's module-local `segments: Blob[]` array,
// which the rotation lifecycle (D-13; SEGMENT_DURATION_MS = 10s,
// MAX_SEGMENTS = 3) populates with self-contained WebM segments.
// After ~35s of continuous recording, A11 asserts count >= 3.
//
// The default getter (`() => 0`) at module load returns 0 — A11
// therefore MUST call this AFTER setupFreshRecording so the
// recorder has wired the live getter. A pre-recording call would
// legitimately return 0; the harness orders the assertion so this
// failure mode is unreachable.
//
// -1 sentinel on error preserves the dispatcher contract (every
// op returns a numeric `count` field on the happy path or -1 +
// `error` on failure). A `try/catch` is defensive against a future
// getter that throws (the closure-bound module-level array is a
// pure read, so no throw is expected, but bridge handlers should
// never propagate exceptions to chrome.runtime.sendMessage).
try {
sendResponse({ count: segmentCountGetter() });
} catch (err) {
sendResponse({
count: -1,
error: err instanceof Error ? err.message : String(err),
});
}
return false;
}
sendResponse({ ok: false, error: 'unknown-op' });
return false;
});
// ─── Auto-install fake getDisplayMedia at module load ────────────────
// PROTOTYPE: install the fake getDisplayMedia eagerly so production
// recorder.startRecording will use the synthetic stream on its first
// call — no chicken-and-egg with the bridge install op. Wrap in a
// try so any DOM-not-ready edge case does not block module init.
try {
installFakeDisplayMedia();
} catch (e) {
console.warn("[offscreen-hooks] eager installFakeDisplayMedia failed:", e);
}
export {};