# Capture experiment

The maintained study now uses the [plugin contract](../architecture/plugins.md): custom Start/Record/Settings sections, host-owned selection/settings bindings, one browser instance per attempt and a separate local-export delivery run. Native retained sharing observes completion, and cancelled-attempt callbacks cannot finish a replacement attempt.

Implementation #108 informs human-led #83. Selection, timing and stream lifetime follow the [accepted capture decision](https://github.com/CosticaPuntaru/SnagBadger/issues/83#issuecomment-6150412698); observed prototype acceptance remains open. It reuses accepted #81 capture/runtime/evidence contracts and the canonical local report bundle; it does not integrate diagnostics, the reporting drawer, screen video, annotation or tracker submission.

Run `pnpm dev:examples`, then `playground/capture-study/`. The first enabled screenshot/recording adapter in resolved configuration order is preselected. Choose one alternative per enabled category, optionally select structured interactions and recording, and Start. Screen video and DOM replay share one recording category; the study offers real rrweb and a clearly labelled shared-stream recording fixture that produces no video. Controls hide before capture. Recording selections expose Stop/Cancel; screenshot-only captures immediately. The profile fixture includes canvas, an iframe, a missing image and private/excluded regions so limitations remain visible.

The separate **Start sharing experiment** lets the user compare one-shot sharing with a retained stream: capture another frame from the same explicitly live stream, then End sharing. It does not persist browser approval or survive reload. The default native request uses the actual browser picker. Canvas-stream/denied/unsupported choices are deterministic fixtures, clearly labelled; they prove encoding/failure/cleanup paths, not actual browser approval or native source fidelity. Both lifetime choices always stop acquired tracks on disposal/cancellation, including acquisition that resolves late. The accepted runtime disposes captures on session finish, so retained sharing within this experiment is not retention across report sessions.

## Consumption

The portable `@snagbadger/capture` entry retains only canonical types. Selected browser adapters live at `@snagbadger/capture/browser`. Import and factory construction install no instrumentation. DOM rendering, rrweb recording and rrweb playback libraries load on activation, collection or explicit playback; ordinary functions-only consumers load no browser library, React or CSS. Adapters have no intrinsic registration ID and can be wrapped/replaced through the accepted configuration registrations. Factory options define developer-authorized policy; do not expose arbitrary privacy changes through reporter settings.

- `createDomScreenshot({ target, privacy })` reconstructs a selected host subtree. Inputs and mask/exclude regions are omitted entirely, including private roots and masked ancestors. Iframes are always omitted because nested-document privacy cannot be guaranteed by the renderer. It records canvas/iframe/missing-image counts and limitations alongside the PNG. DOM reconstruction may lose WebGL, inaccessible assets, fonts and browser compositing. Missing assets do not magically become available.
- `createDisplayScreenshot({ lifetime, allowUnmaskedPixels, request? })` captures a live chosen surface into a PNG. Both lifetime and explicit raw-pixel permission are required. DOM privacy selectors cannot sanitize display pixels; choose an appropriate surface with synthetic data. A fresh request always requires browser approval. The optional request seam accepts a custom display-stream acquisition adapter; it does not emulate saved permission.
- `createInteractionRecorder({ target, privacy, limit })` observes click/input/change/submit with UTC times and optional developer-supplied `data-capture-id` targets. It never stores values or element text; excluded regions are ignored. Bounds report dropped interactions. It does not author reproduction steps or executable scripts, and does not promise cross-navigation continuity.
- `createVisualRecorder({ privacy, maxEvents })` records rrweb DOM history, masks inputs and selected text, blocks excluded regions, and disables canvas and cross-origin iframe recording. Available images/styles are inlined through rrweb; the envelope preserves exact recorder/format version, asset strategy/limitations and retention truncation. Fonts/CORS/missing assets are not a complete portable asset archive. Input masking does not remove values a host renders elsewhere as ordinary text: developers must select those regions for text masking or exclusion too.
- `openVisualReplay(root, bytes)` validates version, size, bounded JSON depth, event order, metadata, serialized node structure/attributes/children/text, snapshot offsets, and supported incremental payloads before creating rrweb's sandboxed replay iframe. Incompatible versions, malformed/unsupported payloads and missing snapshots fail explicitly. Plugins, canvas/font/custom events are outside this recorder's supported subset. Playback is observed DOM history, not pixel video or executable reproduction. `play`, `pause` and `dispose` own the replay resource.

Export writes the existing report bundle into a ZIP for transport. Import bounds archive/decompressed size, decodes canonical text files and calls the existing strict bundle importer. It never executes bundle HTML. The replay-only route `?view=replay` has no original host fixture; uploading the exported ZIP there demonstrates independent reconstruction. Assets that cannot be inlined remain a fidelity limitation; the study does not promise universal offline fidelity.

## Local lifecycle and recovery

The study's local lifecycle module owns the reporting runtime, retained sharing, replay preparation and publication, and screenshot/download URLs. DOM handlers select adapters once per attempt and render its state; they do not own pending generations or resources. Retained sharing reuses the reporting runtime's cancellation and late-acquisition cleanup. A selected capture failure settles the attempt immediately, stops every selected mode, discards this attempt's evidence and shows the error with Retry. Prior report output/drafts remain intact. Diagnostic-harvester failures still isolate and retain valid output. The runtime's completed promise also settles on resource-originated completion, including native Stop sharing.

Capture, sharing approval/frame collection, and replay loading expose cancellation. Cancel restores usable controls without waiting for the browser picker or a late replay loader; the browser's own picker may still need dismissal. Late resources are released and cannot publish. Retry is explicit and reads current visible capture/lifetime choices. Replay import keeps the labelled last valid result until a replacement succeeds; failed or cancelled replacements leave it available. A successfully imported report without recording replaces the prior report and truthfully disables playback. Replay uses Start/Stop, and every Start begins again; capture recording controls are unchanged. Leaving the page releases owned resources and URLs.

## Verification and human review

Public-decoder browser regressions reject the exact malformed snapshot reported in review plus malformed mutation/input/movement/style events. Browser scenarios prove mixed real capture/export/import, explicit failures/cancellation, PNG bytes, snapshot/retained stream behavior, late-acquisition cleanup and rejected corrupt playback. Full repository verification and observed source-bound T3 evidence must be bound to the final commit before handoff. Accepted behavior and native picker/fidelity observations remain distinct from deterministic tests. #83 stays open until actual feedback resolves it; merging experiment code alone cannot close it.

## Dependency declarations

Exact packages: `html-to-image` 1.11.13, `@rrweb/record` and `@rrweb/replay` 2.1.7. The rrweb recorder and player are separately loaded as recommended by [rrweb's guide](https://rrweb.com/docs/guide); DOM filtering follows [html-to-image's supported filter](https://github.com/bubkoo/html-to-image#filter). These library choices implement an experiment, not an accepted capture default.

Published rrweb/rrdom 2.1.7 declaration files fail the repository's strict TypeScript 6 gate. Two version-bound pnpm patches correct exported namespace merging and ambient enum property types in `.d.ts`/`.d.cts` only. Runtime files are unchanged, `skipLibCheck` stays false, and all strict checks remain enabled. Preserve these patches until an upstream release with valid declarations is verified. The libraries and transitive packages retain their published licenses.

## Category selection and shared screenshots

Registration `id` identifies one implementation; optional `category` groups alternatives. Identical registration IDs retain complete last-registration replacement. Distinct IDs in the same category coexist. `captureCategories` on `@snagbadger/reporting/session` lists enabled alternatives and their first configured default without loading them. The runtime rejects multiple selections within a capture category; uncategorized registrations use their registration ID as their effective category, consistently in discovery and validation. Legacy registrations with distinct effective categories remain independently selectable.

`createDisplaySource` on `@snagbadger/capture/browser` owns one acquisition shared by screenshot/recording consumers. It retains one latest frame at 100 ms intervals, bounded to 16 megapixels. Final release stops tracks, sampling and playback, including late acquisition. Native Stop sharing finishes the attempt and encodes the latest retained frame rather than requesting another stream. This last frame can precede the native click. Canvas-stream native-end simulation proves the lifecycle, not actual browser-picker approval.

Screenshot-only takes one frame and closes sharing. Steps plus browser screenshot acquires at Start, takes a single ending screenshot on Stop, then closes sharing. Recording plus screenshot defaults to end-only. The optional `createActionScreenshots` wrapper observes non-excluded click/change/submit events after synchronous host handlers, with no stored input values or attached interactions. It does not require any steps adapter. The study limits action frames to 100 plus the ending frame; retained attachment bytes are capped at 32 MiB. It does not promise async-render settlement, navigation continuity or screenshots for unobserved events. Browser pixels require explicit raw-pixel permission; DOM masking cannot sanitize them.

The standalone sharing experiment's lifetime control remains an explicit comparison tool. The normal capture flow derives lifetime from the selected modes; choosing snapshot in the standalone control cannot prematurely close a steps/recording session.

Per-action composition requires a snapshot adapter declaring `repeatable: true`. DOM screenshots declare it; display screenshots declare it only for session lifetime. The wrapper rejects one-shot or recording adapters before acquisition, so a first action cannot inadvertently close sharing needed by later actions. Custom screenshot adapters must explicitly satisfy the same repeated-collection and disposal contract.
