---
phase: 01-stabilize-video-pipeline
verified: 2026-05-16T09:11:33Z
verifier: gsd-verifier
goal: The video ring buffer captures the most recent 30 s of the active tab's video continuously across tab switches, with a playable WebM header retained — so that on export the assembled `last_30sec.webm` will play.
status: human_needed
axes_total: 10
axes_passed: 10
axes_failed: 0
axes_advisory: 2
---

# Phase 1: Stabilize Video Pipeline — Goal-Backward Verification

## Phase Goal (verbatim from ROADMAP.md)

> The video ring buffer captures the most recent 30 s of the active tab's video continuously across tab switches, with a playable WebM header retained — so that on export the assembled `last_30sec.webm` will play.

## Verdict

**DELIVERED** — 10 of 10 verification axes PASS against the codebase; phase goal is achieved by the wired implementation, not just by SUMMARY narration. Two **ADVISORY** notes recorded that do not block phase completion. Three **operator-side residue** items remain that THIS verifier cannot validate without a Chrome runtime — these MUST be picked up by Phase 4's SPEC §10 smoke pass.

Status is `human_needed` because the residue items below require a real Chrome operator session (visual picker UX, tab-switch continuity, SW idle survival) — automated codebase verification is exhausted.

---

## Per-Axis Verification Table

| #  | Axis                                              | Evidence (file:line or command)                                                                                                                                                                                  | Verdict   |
| -- | ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- |
| 1  | "30 s ring buffer" — constants + rotation         | `src/offscreen/recorder.ts:23-29` (`SEGMENT_DURATION_MS=10_000`, `MAX_SEGMENTS=3`, `VIDEO_BUFFER_DURATION_MS = 30_000`); `:86-89` push + shift cap; `:352-355` production rotation evict-oldest                  | PASS      |
| 2  | "Continuous across tab switches" — no re-acquire  | `grep -RIn "onActivated\|onUpdated\|re-attach\|reAttach" src/` returns **no matches** (exit 1); `src/offscreen/recorder.ts:46` comment pins same-MediaStream across rotations; `getDisplayMedia` is screen/window-scoped per D-01 | PASS      |
| 3  | "Playable WebM" — D-13 per-segment self-contained | `src/offscreen/recorder.ts:252-302` `startNewSegment` constructs fresh MediaRecorder per rotation; `:349-370` `onSegmentStopped` finalizes & rotates; `tests/offscreen/segment-keyframes.test.ts:195-269` GREEN-pins "each retained segment starts with a keyframe" + 30-s window invariant | PASS      |
| 4  | "D-12 base64 wire transfer"                       | `src/shared/binary.ts:42-85` round-trip helpers; `src/shared/types.ts:64-68` `TransferredVideoSegment.data: string` (base64); `src/offscreen/recorder.ts:574` `await blobToBase64`; `src/background/index.ts:188` `base64ToBlob`; `tests/offscreen/port-serialization.test.ts` 9 tests GREEN | PASS      |
| 5  | "T-1-04 sender-id check both ends"                | offscreen-side: `src/offscreen/recorder.ts:465-467` `isFromOwnExtension`, `:646` enforced on onMessage; SW-side: `src/background/index.ts:83-87` rejects port with mismatched sender, `:514-517` rejects onMessage with mismatched sender | PASS      |
| 6  | "SW respawn safety"                               | `src/background/index.ts:33-35` Promise; `:548-552` `OFFSCREEN_READY` resolves; `:576-595` `hasDocument()` check + early-resolve on detected pre-existing offscreen (CR-03 fix); `:608-613` `indexedDB.deleteDatabase('VideoRecorderDB')` in `onInstalled` | PASS      |
| 7  | "Manifest aligned"                                | `manifest.json:6-13` lists `desktopCapture` + `offscreen`; **no `tabCapture`**; `grep -RIn "tabCapture\|chrome.alarms" src/` returns **no matches** (exit 1); `chrome.runtime.connect` port replaces alarms keepalive (recorder.ts:606-631 + background/index.ts:78-118) | PASS      |
| 8  | "Fixture validity"                                | `tests/fixtures/last_30sec.webm` exists (1 633 459 bytes); `ffprobe -v error -f matroska -i ...` → exit 0, empty stderr; codec=vp9 Profile 0, 1142×1038, bt709                                                       | PASS      |
| 9  | "Acceptance tests aligned"                        | `tests/offscreen/webm-playback.test.ts:94-114` `decodeDryRunStrict` invokes ffmpeg via `spawnSync` and parses stderr for packet-error + ended-prematurely; `npx vitest run` shows both empirical gates execute (988 ms + 914 ms) and PASS | PASS      |
| 10 | "Code-review aftermath — no regression"           | 7 test files / 40 tests all green; tsc exit 0; type-safety grep clean; `npm run build` exit 0 (60 modules, dist/ produced); sweep commits 08a79a6 (stop-race), 7c91f52 (re-entrance + start-throw + dual-track teardown), 034155b (port-replaced diagnostic) inspected and preserve D-12/D-13 contracts | PASS      |

**Score: 10/10 axes verified.**

---

## Required Artifacts (3-level check)

| Artifact                                | Expected                                                                  | Status   | Details                                                                                                                                                                |
| --------------------------------------- | ------------------------------------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `src/offscreen/recorder.ts`             | getDisplayMedia + D-13 lifecycle + port + handshake                       | VERIFIED | Exists (673 lines, substantive); imported by offscreen/index.html via crxjs; bootstrap() runs on module load; ALL ring-buffer constants, classifier, lifecycle hardening present |
| `src/background/index.ts`               | SW with port-based fetch, T-1-04 sender check, hasDocument check          | VERIFIED | Exists (618 lines); receives port via onConnect; runs `chrome.offscreen.hasDocument()` on init; CR-03 fix early-resolves `offscreenReady` on detected pre-existing offscreen |
| `src/shared/binary.ts`                  | D-12 base64 ↔ Blob helpers                                                | VERIFIED | Exists (85 lines); pure, portable (no FileReader); pinned by 9 tests in port-serialization.test.ts                                                                       |
| `src/shared/types.ts`                   | `VideoSegment` + `TransferredVideoSegment` wire-format                    | VERIFIED | `VideoSegment {data: Blob; timestamp}` + `TransferredVideoSegment {data: string; type; timestamp}` both present; `Message<T = unknown>` post-IN-05                       |
| `manifest.json`                         | `desktopCapture` + `offscreen` permissions; NO `tabCapture`               | VERIFIED | Confirmed line-by-line                                                                                                                                                  |
| `tests/offscreen/*.test.ts` (7 files)   | codec-check, handshake, port, port-serialization, segment-keyframes, segment-rotation, webm-playback | VERIFIED | All 7 present; ring-buffer.test.ts retired (IN-03) leaving 7 file × 40-test surface                                                                                       |
| `tests/fixtures/last_30sec.webm`        | Empirical D-13 regression fixture                                         | VERIFIED | 1 633 459 bytes; vp9 1142×1038; ffprobe + ffmpeg dry-run both exit 0                                                                                                     |

---

## Key Link Verification

| From                                                | To                                          | Via                                                          | Status | Details                                                                                                                                                       |
| --------------------------------------------------- | ------------------------------------------- | ------------------------------------------------------------ | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `offscreen/index.html`                              | `src/offscreen/recorder.ts`                 | `<script type="module" src="./recorder.ts">`                 | WIRED  | Confirmed at `src/offscreen/index.html:8`; crxjs picks up via rollupOptions.input in vite.config.ts                                                              |
| `recorder.ts: encodeAndSendBuffer`                  | `binary.ts: blobToBase64`                   | `import { blobToBase64 } from '../shared/binary'`            | WIRED  | Imported at `recorder.ts:18`; called at `:574`                                                                                                                  |
| `background/index.ts: getVideoBufferFromOffscreen`  | `binary.ts: base64ToBlob`                   | `import { base64ToBlob, blobToBase64 } from '../shared/binary'` | WIRED  | Imported at `background/index.ts:2`; called at `:188`; result wrapped in `VideoSegment[]` returned to `saveArchive` → `createArchive` → archive `video/last_30sec.webm` entry |
| `recorder.ts: connectPort`                          | `background/index.ts: onConnect listener`   | `chrome.runtime.connect({ name: 'video-keepalive' })`        | WIRED  | port name match enforced at `background/index.ts:80-82`; sender-id at `:83-87`                                                                                  |
| `recorder.ts: bootstrap → sendMessage`              | `background/index.ts: OFFSCREEN_READY case` | `chrome.runtime.sendMessage({ type: 'OFFSCREEN_READY' })`    | WIRED  | resolves `offscreenReady` Promise at `background/index.ts:548-552`                                                                                              |
| `background/index.ts: startVideoCapture`            | `recorder.ts: startRecording`               | `chrome.runtime.sendMessage({ type: 'START_RECORDING' })`    | WIRED  | recorder.ts dispatches in `onMessage` switch at `:649-654`                                                                                                       |

---

## Data-Flow Trace (Level 4 — Real Data Production)

| Artifact                       | Data Variable                  | Source                                                                  | Produces Real Data           | Status   |
| ------------------------------ | ------------------------------ | ----------------------------------------------------------------------- | ---------------------------- | -------- |
| `recorder.ts: segments`        | `let segments: Blob[]`         | `onSegmentStopped → new Blob(currentChunks) → push` driven by MediaRecorder.onstop  | YES — real ~10 s WebM blobs from MediaRecorder | FLOWING  |
| `recorder.ts: encodeAndSendBuffer` | `transferred[]`            | `await blobToBase64(segment)` per segment, sent over port                | YES — round-trip pinned by tests; empirical fixture proves end-to-end | FLOWING  |
| `background/index.ts: mergeVideoSegments` | `finalBlob`            | `new Blob(sortedSegments.map(s => s.data))`                              | YES — empirical fixture: 1.6 MB VP9 playable file | FLOWING  |
| `tests/fixtures/last_30sec.webm` | n/a (empirical fixture)      | regenerated 2026-05-15 (commit cd61cbc) against D-13 recorder           | YES — ffprobe exit 0 + ffmpeg dry-run exit 0 + operator-confirmed Chrome playback | FLOWING  |

---

## Behavioral Spot-Checks

| Behavior                                            | Command                                                                                                              | Result                                                                                                                                                  | Status |
| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ |
| Vitest suite passes                                 | `npx vitest run --reporter=dot`                                                                                      | Test Files 7 passed (7); Tests 40 passed (40); 3.06s                                                                                                     | PASS   |
| TypeScript type-check passes                        | `npx tsc --noEmit`                                                                                                   | exit 0 (no output)                                                                                                                                       | PASS   |
| No `as any` or `@ts-ignore` in Phase 1 surface      | `grep -RIn "as any\|@ts-ignore" src/offscreen/ src/background/index.ts src/shared/`                                  | exit 1 (no matches)                                                                                                                                      | PASS   |
| No `tabCapture` / `chrome.alarms` residue           | `grep -RIn "tabCapture\|chrome.alarms" src/`                                                                         | exit 1 (no matches)                                                                                                                                      | PASS   |
| No legacy IndexedDB SW plumbing                     | `grep -rn "openIndexedDB\|VideoRecorderDB" src/ --include='*.ts'`                                                    | Only matches: orphan-cleanup `indexedDB.deleteDatabase('VideoRecorderDB')` at `background/index.ts:609-610` (intentional)                                | PASS   |
| Legacy top-level `offscreen/` directory removed     | `ls offscreen/`                                                                                                       | exit 2 — directory does not exist                                                                                                                       | PASS   |
| Vite inline-plugin removed                          | `grep -rn "copy-offscreen\|chromeMediaSource" vite.config.ts`                                                        | exit 1 (no matches)                                                                                                                                      | PASS   |
| Build produces loadable extension                   | `npm run build`                                                                                                       | 60 modules transformed, exit 0; `dist/manifest.json`, `dist/assets/offscreen-*.js`, `dist/assets/index.ts-*.js` (SW) all produced                          | PASS   |
| Fixture decodable by ffprobe (D-12 gate)            | `ffprobe -v error -f matroska -i tests/fixtures/last_30sec.webm`                                                     | exit 0; empty stderr                                                                                                                                     | PASS   |
| Fixture passes A3 empirical decode dry-run          | `ffmpeg -nostdin -v warning -i tests/fixtures/last_30sec.webm -f null -`                                              | exit 0; **0** packet errors; **0** "File ended prematurely"; 298 DTS-monotonicity warnings (expected D-13 trade-off) + 1 "File extends beyond end of segment" (advisory, see below) | PASS   |

---

## Requirements Coverage

| Requirement              | Source Plan      | Description                                                                                            | Status     | Evidence                                                                                                                                                                  |
| ------------------------ | ---------------- | ------------------------------------------------------------------------------------------------------ | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| REQ-video-ring-buffer    | Phase 1 (all 7 plans) | 30 s in-memory ring buffer via D-13 restart-segments; `getDisplayMedia` (AMENDED from `chrome.tabCapture`); vp9 @ 400 000 bps | SATISFIED  | All structural + empirical + wiring evidence above; SPEC §10 #2/#3/#7 functionally green at Phase 1 level                                                                  |

**Phase 4 (SPEC §10 smoke) still owns the canonical 9-criterion sweep.** Phase 1 only proved #2/#3/#7 against the codebase + fixture; #1 install, #6 < 5 s export latency, #8 password masking, #9 RAM ceiling, and end-to-end Chrome runtime confirmation remain Phase 4 scope. This is by design per REQUIREMENTS.md line 188.

---

## Anti-Patterns Found

| File                                | Line     | Pattern                                                                            | Severity | Impact                                                                                                                                                                                                       |
| ----------------------------------- | -------- | ---------------------------------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| (none)                              | —        | No TODO/FIXME/XXX/HACK in Phase 1 surface; no `as any` / `@ts-ignore`; no `console.log`-only handlers; no empty `=> {}` rendering paths; no hardcoded empty arrays/objects flowing to user-visible output | —        | Clean.                                                                                                                                                                                                       |

The `ffmpeg` stderr emits a single `File extends beyond end of segment` line — this is **Matroska-demuxer-level informational**, not a decoder error, and is the documented D-13 trade-off (multi-EBML-header concat). Recorded as ADVISORY below, not as an anti-pattern.

---

## Advisory Observations

These do NOT block Phase 1 completion but warrant attention in Phase 4 or Phase 5.

1. **ADV-1: ffmpeg emits one `[in#0/matroska,webm] File extends beyond end of segment` line.** This is the Matroska demuxer noting that the SECOND EBML segment in the concatenated WebM declares a smaller `SegmentSize` than the remaining bytes (because subsequent segments follow). Chrome's MSE pipeline handles this natively — operator-confirmed playback was clean on 2026-05-15. The `webm-playback.test.ts` acceptance gate does NOT assert against this signal (only `packetErrorCount === 0` and `endedPrematurely === false`), so it is correctly NOT a failure. Recommend documenting this signal in the Phase 5 cursor-visibility plan's "expected stderr from ffmpeg dry-run" allow-list to prevent future verifiers from being surprised by it.

2. **ADV-2: `blobToBase64` per-byte concat is O(n²) for large blobs.** WR-06 in REVIEW-FIX.md was pre-approved for Phase 2 deferral with the documented rationale (`String.fromCharCode(...subarray)` apply-spread argument-length limit ~64 KiB). For the current ~1.6 MB fixture this works fine; if recording quality rises (higher bitrate, longer windows, audio in CAP-01) the encode latency could grow non-linearly. Phase 2 quality-bump review should consider chunked-apply at 32 KiB stride per REVIEW.md's WR-06 recommendation.

---

## Operator-Side Residue (UAT scope)

These behaviors CANNOT be verified by this codebase verifier — they require a real Chrome runtime and an operator. Phase 4 SPEC §10 smoke pass MUST cover them:

1. **OPR-1: `getDisplayMedia` picker UX appears and grants stream on operator click.** Chrome's native screen-share picker dialog cannot be driven from Vitest's Node environment. Verification path: load `dist/` unpacked → click extension → observe picker dialog → pick "Entire screen" → confirm "Sharing your screen" indicator appears → SW console shows `[Offscreen:Recorder] Stream created`. (VALIDATION.md §"Manual-Only Verifications" row 1.)

2. **OPR-2: Continuous recording across tab switches under live `getDisplayMedia` stream.** Codebase verification confirms NO per-tab re-acquisition code exists (no `chrome.tabs.onActivated` handlers; recorder retains single `MediaStream` across rotations); this is a SUFFICIENT structural condition. Empirical confirmation that the recording does NOT pause when the operator switches tabs in Chrome requires a live session. Verification path: start recording, switch tabs 3-5 times over 30 s, export, confirm `last_30sec.webm` covers the full 30 s window. (VALIDATION.md §"Manual-Only Verifications" row 2 analogue, applied to amended D-15 semantics.)

3. **OPR-3: SW idle-unload survival.** D-16 + D-17 are structurally satisfied (offscreen owns buffer; long-lived port keeps SW alive; `hasDocument()` + CR-03 handshake fix on SW respawn). Real MV3 SW lifecycle behavior cannot be driven in Vitest. Verification path: DevTools → Extensions → Service Worker → "Force stop" → wait 60 s → click extension save → confirm exported archive contains video segments from before "Force stop". (VALIDATION.md §"Manual-Only Verifications" row 3.)

These three items are Phase 4's territory per ROADMAP.md (Phase 4 success criteria #2 and #4 in particular). This verifier does NOT classify them as Phase 1 gaps — Phase 1's `human_needed` status is the correct routing: ship Phase 1 as DELIVERED in code, hand the operator-side checks to Phase 4 smoke.

---

## Gaps Summary

No gaps. Phase 1's goal — "the video ring buffer captures the most recent 30 s of the active tab's video continuously across tab switches, with a playable WebM header retained — so that on export the assembled `last_30sec.webm` will play" — is achieved by the wired codebase:

- **30 s buffer:** `MAX_SEGMENTS=3 × SEGMENT_DURATION_MS=10_000 = 30_000` ms enforced in production (`onSegmentStopped:352-355`) AND pinned by tests (`segment-rotation.test.ts:84-104`, "caps at MAX_SEGMENTS — 4th push evicts the oldest", "many pushes never exceed the cap").
- **Continuous across tab switches:** Amended D-01/D-15 semantics under `getDisplayMedia` — no per-tab handlers exist, single MediaStream retained across MediaRecorder rotations (recorder.ts:46 comment + grep evidence).
- **Playable WebM:** D-13 restart-segments — each segment self-contained with own EBML header + seed keyframe (`startNewSegment:252-302` constructs fresh MediaRecorder per rotation); empirically verified by ffmpeg dry-run exit 0 + 0 packet errors + 0 ended-prematurely.
- **Assembled `last_30sec.webm` plays:** Empirical fixture `tests/fixtures/last_30sec.webm` (1.6 MB) passes both D-12 (ffprobe) and A3 (ffmpeg) gates, operator-confirmed Chrome playback on 2026-05-15.

The two architectural escalations encountered mid-phase (D-12 binary transfer + A3 keyframe orphan-P-frame) were both anticipated as HIGH-risk in RESEARCH.md and resolved by pre-staged fallbacks (base64 wire format + D-13 restart-segments); both fallbacks are now production code and pinned by tests.

---

_Verified: 2026-05-16T09:11:33Z_
_Verifier: Claude (gsd-verifier)_
