diff --git a/.planning/phases/01-stabilize-video-pipeline/01-08-SUMMARY.md b/.planning/phases/01-stabilize-video-pipeline/01-08-SUMMARY.md new file mode 100644 index 0000000..a19baa4 --- /dev/null +++ b/.planning/phases/01-stabilize-video-pipeline/01-08-SUMMARY.md @@ -0,0 +1,210 @@ +--- +phase: 01-stabilize-video-pipeline +plan: 08 +subsystem: video-pipeline +tags: [webm, remux, ts-ebml, webm-muxer, vp9, matroska, ebml, service-worker] + +# Dependency graph +requires: + - phase: 01-stabilize-video-pipeline + provides: D-13 restart-segments recorder lifecycle (Plan 01-07) — produces 3 self-contained ~10 s WebM segments that this plan remuxes into a single-EBML output. + - phase: 01-stabilize-video-pipeline + provides: Option C port lifecycle + EmptyVideoBufferError surface (D-17-port-lifecycle amendment) — preserves the operator-visible failure mode this plan extends to zero-byte remux output. +provides: + - "single-EBML-headered WebM output via remuxSegments(segments): Promise" + - "ts-ebml ^3.0.2 + webm-muxer ^5.1.4 dependencies (both MIT, both SW-compatible)" + - "CONTEXT.md amendment D-14-remux superseding D-13 file-concat (recorder lifecycle preserved)" + - "5 new unit tests pinning single-EBML / monotonic-timestamp / frame-count / size-sanity / empty-input invariants" + - "2 import-shape tests pinning library SW-compatibility" +affects: + - phase: 04-spec-smoke-verification + note: "SPEC §10 #7 (last_30sec.webm plays in browser) is now functionally satisfied at the codebase level pending the Task 5 operator checkpoint." + +# Tech tracking +tech-stack: + added: + - "ts-ebml ^3.0.2 — EBML parser (Decoder + tools.ebmlBlock for SimpleBlock decode)" + - "webm-muxer ^5.1.4 — single-EBML WebM writer (Muxer.addVideoChunkRaw)" + patterns: + - "Parse-extract-remux pipeline: decode each segment via ts-ebml → walk Cluster/SimpleBlock tree → re-emit through webm-muxer with monotonic timestamps adjusted via accumulating segmentBaseMs." + - "Empty-input defensive guard: remuxSegments([]) returns a Promise with .size === 0; upstream EmptyVideoBufferError throw catches it." + - "Library surface pinning via dedicated deps-import test that asserts named exports AND DOM-global absence at import time — guards against future bumps acquiring window/document references." + +key-files: + created: + - "src/background/webm-remux.ts (434 LOC) — remuxSegments() + helpers" + - "tests/background/webm-remux.test.ts (368 LOC) — 5 unit tests" + - "tests/background/webm-remux-deps.test.ts (137 LOC) — 2 SW-compat tests" + modified: + - "src/background/index.ts — mergeVideoSegments deleted; await remuxSegments swapped in; EmptyVideoBufferError detail string updated to 'remuxed video blob is zero bytes'" + - "package.json + package-lock.json — added ts-ebml ^3.0.2 + webm-muxer ^5.1.4" + +key-decisions: + - "D-14-remux: ts-ebml (parse) + webm-muxer (write) is the smallest fix matching the problem shape; we don't re-encode VP9 frames, we just re-container them with monotonic timestamps. WebCodecs path rejected as over-engineered (would re-encode for zero quality benefit); cluster-aware-trim revisit rejected as architecturally weaker (non-deterministic content window)." + - "webm-muxer 5.1.4 upstream-deprecated in favor of Mediabunny; the pinned version still meets the d13 architectural requirement (single-EBML output via addVideoChunkRaw). Migration to Mediabunny is out of scope for Plan 01-08 (would require new ADR)." + - "I-01 frame-count tolerance tightened to [905, 912] (±3 absolute frames matching 3 segment boundaries × 1 partial frame each) from the looser ±20% band that would have accepted catastrophic frame loss." + - "B-01 CONTEXT.md amendment provenance verified via 4 grep checks in Task 4 (no file mutation needed — the orchestrator already appended D-14-remux at plan-creation time in commit 2e499d7)." + +patterns-established: + - "Single-EBML-header remux pattern: parse N self-contained WebM segments via ts-ebml → extract VP9 SimpleBlocks + cluster timestamps + keyframe flags → re-emit through webm-muxer with adjusted monotonic timestamps. Generalizes to any future multi-segment WebM consolidation need." + - "Defensive track-info derivation: walk Tracks→TrackEntry→Video subtree of first segment for PixelWidth/PixelHeight; fallback to documented defaults (1024×768) with a logged warning if no Video subtree found." + +requirements-completed: + - REQ-video-ring-buffer + +# Metrics +duration: 6min +completed: 2026-05-17 +--- + +# Phase 1 Plan 08: WebM Remux via ts-ebml + webm-muxer Summary + +**Single-EBML-headered WebM output via remuxSegments() — replaces D-13 file-concat (which mpv/Chrome/ffprobe truncated to 9.94 s) with a true multi-segment remux producing the full 29.954 s timeline that SPEC §10 #7 actually requires.** + +## Performance + +- **Duration:** 6 min (Tasks 1-4 autonomous; Task 5 awaits operator) +- **Started:** 2026-05-17T07:21:43Z +- **Completed:** 2026-05-17T07:28:03Z (Tasks 1-4; Task 5 checkpoint awaiting operator) +- **Tasks:** 4 of 5 autonomous tasks complete; Task 5 = `checkpoint:human-verify` (operator-driven fixture regen via smoke.sh) +- **Files modified:** 6 (3 source/test files created, 2 source files modified, package.json + package-lock.json updated) + +## Accomplishments + +- **D-13 file-concat retired** — `mergeVideoSegments()` deleted from `src/background/index.ts`; only a retirement comment naming Plan 01-08 D-14-remux remains. +- **Single-EBML WebM remux landed** — `src/background/webm-remux.ts:remuxSegments()` walks each segment's EBML tree via ts-ebml Decoder, extracts every VP9 SimpleBlock's frame bytes + keyframe flag + segment-local timestamp, and re-emits through webm-muxer's `Muxer.addVideoChunkRaw` with adjusted monotonic timestamps. Empirically: 912 frames spanning 29.954 s = ~30 s of content, 1.6 MB output. +- **EmptyVideoBufferError surface preserved** — the Option C operator-visible failure mode (`error.code === 'empty-video-buffer'`) carries through; only the free-text detail string changed from "merged video blob is zero bytes" to "remuxed video blob is zero bytes" (W-01 pre-flight grep confirmed no downstream consumer matches the legacy string). +- **CONTEXT.md amendment provenance verified intact** — the 4 grep checks (D-14-remux marker, original D-13 line, D-17-port-lifecycle amendment, webm-remux.ts citation) all return exactly 1 match; no re-append needed (B-01 fix from plan checker pass). +- **Library SW-compatibility pinned** — `tests/background/webm-remux-deps.test.ts` asserts both libraries import cleanly with `window`/`document` absent on `globalThis`, guarding future bumps against acquiring DOM globals that would crash the Chrome service-worker runtime. +- **All 53 baseline tests + 7 new unit tests GREEN** at the Task 4 boundary (62 total tests across 13 files). Only the 2 webm-playback duration assertions remain RED — they read the stale committed fixture and will flip GREEN when Task 5 lands a regenerated fixture. + +## Task Commits + +Each task was committed atomically using `--no-verify` (worktree-mode parallel-execution discipline): + +1. **Task 1: Install ts-ebml + webm-muxer; pin SW-compat** — `5035314` (feat) — Added both deps at pinned versions, created the 2-test SW-compat import-shape spec, baseline 55 GREEN. +2. **Task 2: RED unit tests for remuxSegments invariants** — `407e683` (test) — 5 RED tests (single-EBML, size sanity, ffprobe duration >= 25 s, frame count [905, 912], empty input) all failing with module-missing message; baseline 55 GREEN preserved + 2 webm-playback RED + 5 new RED = 13 files / 62 tests / 7 failed. +3. **Task 3: Implement remuxSegments — GREEN** — `41e94d5` (feat) — 434 LOC implementation; all 5 RED flipped GREEN; full suite 60 GREEN + 2 RED (webm-playback duration still gated on Task 5 fixture regen); tsc exit 0. +4. **Task 4: Swap mergeVideoSegments → await remuxSegments; verify CONTEXT.md amendment** — `35db6c2` (feat) — call site swap; mergeVideoSegments deleted (0 non-comment hits); EmptyVideoBufferError throw paths preserved; CONTEXT.md 4-grep verify passed; `npm run build` exit 0 (SW bundle 374.56 KB / 108.44 KB gzipped — matches d13 library survey's ~100 KB estimate); 60 GREEN + 2 RED. + +**Task 5 (checkpoint:human-verify):** PENDING — operator must run smoke.sh against the post-remux build and confirm Chrome + mpv playback ~30 s. See "Next Phase Readiness" below for the operator runbook. + +## Files Created/Modified + +- `src/background/webm-remux.ts` (434 LOC, NEW) — `remuxSegments()` + helpers (`blobToArrayBuffer`, `pickTrackInfoFromSegment`, `extractFramesFromSegment`); extensive JSDoc citing D-14-remux + d13 debug session; uses `Logger('Remux')`; no `as any`, no `@ts-ignore`. +- `tests/background/webm-remux.test.ts` (368 LOC, NEW) — 5 unit tests: single-EBML invariant, size sanity, ffprobe duration >= 25 s (skip-if-no-ffprobe), ffprobe frame count [905, 912] (skip-if-no-ffprobe), empty-input safety. +- `tests/background/webm-remux-deps.test.ts` (137 LOC, NEW) — 2 import-shape tests: named exports surface; both libraries import cleanly without `window`/`document` globals. +- `src/background/index.ts` (modified) — added `import { remuxSegments } from './webm-remux'`; deleted `mergeVideoSegments` body (kept retirement comment); swapped `createArchive` to `await remuxSegments(...)`; renamed error detail string `merged → remuxed`; updated stale `decodeBufferSegments` WR-07 comment to reference the new pipeline. +- `package.json` + `package-lock.json` (modified) — added `ts-ebml ^3.0.2` + `webm-muxer ^5.1.4` to `dependencies`. + +## Decisions Made + +- **Codec metadata fallback**: `EncodedVideoChunkMetadata.decoderConfig` requires both `codec` and `description`. When a CodecPrivate is extracted (rare for MediaRecorder VP9), the implementation attaches `codec: 'vp09.00.10.08'` (canonical WebCodecs string for VP9 Profile 0, Level 1.0, 8-bit — Chrome's published default). When CodecPrivate is absent (the typical case for MediaRecorder output), no meta is passed and webm-muxer derives parameters from the first keyframe's superframe header. +- **Two-pass extraction** over single-pass streaming: first pass reads all segments to derive track info from whichever segment exposes it; second pass drives the muxer. Memory cost is ~3 × ~500 KB ≈ 1.5 MB — well within SW heap budget — and code clarity is materially higher than a streaming-with-deferred-config alternative. +- **Cluster Timestamp element name**: ts-ebml's schema uses Matroska v4's `Timestamp` (not the older `Timecode`). Confirmed empirically via segment 1 probe (`Timecode: 0` results returned, `Timestamp: 3` results returned). +- **Master element start/end shape**: `Cluster` (and other masters) yield once per occurrence as `{type:'m', isEnd:false}` then again as `{type:'m', isEnd:true}` during ts-ebml decode. The walk tracks `inCluster` accordingly so child `Timestamp` elements bind to the correct cluster. + +## Deviations from Plan + +### Auto-fixed Issues + +**1. [Rule 1 - Bug] tsc-rejected EncodedVideoChunkMetadata shape** + +- **Found during:** Task 3 (Implement remuxSegments) +- **Issue:** Initial implementation passed `{ decoderConfig: { description: trackInfo.codecPrivate } }` — but `EncodedVideoChunkMetadata.decoderConfig` is `VideoDecoderConfig` which requires both `codec` and `description`. tsc error: `Property 'codec' is missing in type '{ description: Uint8Array... }' but required in type 'VideoDecoderConfig'`. +- **Fix:** Added `codec: 'vp09.00.10.08'` (canonical WebCodecs string for VP9 Profile 0 Level 1.0 8-bit — Chrome's published MediaRecorder default per https://developer.mozilla.org/en-US/docs/Web/Media/Formats/codecs_parameter) alongside `description`. Documented inline that this path is rarely exercised (MediaRecorder-produced VP9 segments omit CodecPrivate; the muxer derives parameters from the first keyframe). +- **Files modified:** `src/background/webm-remux.ts` +- **Verification:** `npx tsc --noEmit` exit 0; all 5 Task 3 tests GREEN; full suite 60 GREEN + 2 RED (webm-playback duration still waiting on Task 5). +- **Committed in:** `41e94d5` (Task 3 commit — fix landed before commit, not as separate commit). + +**2. [Rule 3 - Blocking] Stale `mergeVideoSegments` reference in unrelated comment** + +- **Found during:** Task 4 (call site swap) +- **Issue:** `src/background/index.ts:decodeBufferSegments` carried a WR-07 fix comment referencing the retired `mergeVideoSegments` as the downstream consumer that would mishandle empty wire segments. After deletion the reference became dangling — future `grep mergeVideoSegments` would find this stale comment and the architecture context would be misleading. +- **Fix:** Updated the comment to reference the new remux pipeline (`src/background/webm-remux.ts`) as the downstream consumer; preserved the WR-07 rationale (filter empty segments before base64 decode — wastes a parse cycle). +- **Files modified:** `src/background/index.ts` +- **Verification:** `grep -v '^\s*//' src/background/index.ts | grep -c 'mergeVideoSegments'` returns 0; full suite still 60 GREEN + 2 RED. +- **Committed in:** `35db6c2` (Task 4 commit). + +--- + +**Total deviations:** 2 auto-fixed (1 bug, 1 blocking-style stale reference) +**Impact on plan:** Both fixes essential — Rule 1 was a tsc gate, Rule 3 was a forward-reference cleanup the project's "Tools First" CLAUDE.md doctrine would have caught at the next review anyway. No scope creep; no architectural changes. + +## Issues Encountered + +- **webm-muxer 5.1.4 upstream-deprecated** with a notice to migrate to Mediabunny. The pinned version still functions correctly per the d13 library survey and Plan 01-08's locked dependency choice. Migration to Mediabunny would be a new architectural decision (different API surface, different bundle size profile) requiring a fresh ADR — out of scope for Plan 01-08. Documented in the Task 1 commit body and the package.json dep comment. +- **npm audit reports 4 vulnerabilities** (2 moderate, 2 high) flowing from ts-ebml's transitive `ebml` dependency (last release 2018) and other indirect deps. The vulnerabilities are documented in the d13 library survey as the trade-off of pulling the most-recent ts-ebml (which still depends on the older ebml internals). Phase 5 hardening (per STATE.md `getDisplayMedia` cursor refinement entry) is the natural home for SCA-driven dep updates. Not blocking Plan 01-08 because (a) ts-ebml runs on attacker-influenced-but-extension-internal bytes only (T-1-08-01 disposition: accept; an attacker controlling the input bytes already controls the offscreen MediaRecorder), and (b) bumping ts-ebml to a non-existent newer version would require a custom EBML parser per the Plan 01-08 alternatives matrix. + +## User Setup Required + +None - no external service configuration. The plan's only operator-facing requirement is the Task 5 checkpoint (Chrome + mpv playback verification of the regenerated fixture). See "Next Phase Readiness" for the runbook. + +## Next Phase Readiness + +### Task 5 Operator Runbook (checkpoint:human-verify) + +**This is the only remaining gate.** Tasks 1-4 are autonomous; Task 5 requires the operator to run `smoke.sh` against the new build and visually confirm playback in Chrome + mpv. + +**Run from the worktree** (the post-remux `dist/` is already built at commit `35db6c2`; rebuild only if you make further edits): + +```bash +# From: /home/parf/projects/work/repremium/.claude/worktrees/agent-a9b483f7ebbf25ac0 +npm run build # already done at 35db6c2, but idempotent +KEEP_PROFILE=0 ./smoke.sh +``` + +In the launched Chrome window: +1. Load Unpacked → select `dist/` +2. Click the extension toolbar icon (popup opens, auto-prompts for screen) +3. Screen-share picker auto-accepts the "Mokosh Smoke Test" tab +4. Wait **at least 35 seconds** (longer is fine — wait 5+ minutes if you want to also re-validate the Option C port lifecycle past the 290 s mark) +5. Click the extension icon → "Сохранить отчёт об ошибке" +6. `smoke.sh` detects the new `session_report_*.zip`, extracts `video/last_30sec.webm`, stages it to `/tmp/mokosh-last_30sec.webm` + +**Empirical playback gate (the actual checkpoint):** + +(a) Open `/tmp/mokosh-last_30sec.webm` in Chrome — drag the file into a fresh tab. The video controls MUST report duration approximately 30 s (>= 25 s), NOT ~9 s. + +(b) `mpv /tmp/mokosh-last_30sec.webm` — title bar shows ~30 s duration; playback proceeds the full timeline without stopping early. + +(c) `ffprobe -v error -show_entries format=duration -of csv=p=0 /tmp/mokosh-last_30sec.webm` — reported duration in seconds is between 25.0 and 30.0. + +**If (a)+(b)+(c) PASS:** +```bash +cp /tmp/mokosh-last_30sec.webm tests/fixtures/last_30sec.webm +npx vitest run tests/offscreen/webm-playback.test.ts # all 4 tests MUST flip GREEN +git add tests/fixtures/last_30sec.webm +git commit --no-verify -m "test(01-08): regenerate last_30sec.webm fixture against post-remux build" +``` +Then type "approved" — Plan 01-08 closes; the orchestrator advances Phase 1 markers. + +**If any of (a)/(b)/(c) FAIL:** do NOT replace the fixture. Report the failure mode (duration value, mpv error text, ffmpeg stderr) so Tasks 3-4 can be revised. The most likely failure modes are documented in PLAN.md §Task 5 `` step 9 — covering frameRate mismatches, keyframe-flag parsing off-by-one, CodecPrivate omission, base64 decode failures, and missing track info. + +### Phase 1 closure dependency chain + +- This plan satisfies SPEC §10 #7 (`last_30sec.webm plays back in a browser`) at the codebase level once Task 5 lands. The fixture-regen commit (one operator-driven step away) will flip the 2 RED webm-playback tests GREEN and complete the 13 files / 62 tests / 62 GREEN steady state. +- Plans 01-09 (display-surface + toolbar + badge UX) and 01-10 (onboarding welcome tab) are independent of this plan's call path — both depend only on the upstream offscreen recorder + popup/SW glue. They can land in any order relative to Plan 01-08's Task 5 checkpoint. +- ROADMAP.md and STATE.md updates (REQ-video-ring-buffer flip to complete, Phase 1 close) are the orchestrator's responsibility per `` discipline — this executor does NOT mutate STATE.md / ROADMAP.md. + +## Self-Check: PASSED + +**Files verified present:** +- `src/background/webm-remux.ts` — FOUND +- `tests/background/webm-remux.test.ts` — FOUND +- `tests/background/webm-remux-deps.test.ts` — FOUND +- `src/background/index.ts` (modified) — FOUND +- `package.json` (modified) — FOUND +- `package-lock.json` (modified) — FOUND + +**Commits verified in `git log --oneline --all`:** +- `5035314` (Task 1: deps + SW-compat test) — FOUND +- `407e683` (Task 2: 5 RED unit tests) — FOUND +- `41e94d5` (Task 3: GREEN remux implementation) — FOUND +- `35db6c2` (Task 4: call site swap + CONTEXT.md verify) — FOUND + +All claims in this summary verified against working-tree filesystem and git history. + +--- +*Phase: 01-stabilize-video-pipeline* +*Completed: 2026-05-17 (Tasks 1-4 autonomous; Task 5 awaits operator)*