docs(01-08): write SUMMARY — Tasks 1-4 autonomous complete; Task 5 awaiting operator

- D-14-remux WebM remux pipeline (ts-ebml parse + webm-muxer write)
  replaces D-13 file-concat; single-EBML-headered output empirically
  spans 29.954 s of 912 VP9 frames (matching the per-segment sum
  301+300+311 with zero loss).
- All 5 RED unit tests in tests/background/webm-remux.test.ts flipped
  GREEN; 2 SW-compat tests in webm-remux-deps.test.ts GREEN; 53 baseline
  tests preserved. tsc exit 0. npm run build exit 0.
- mergeVideoSegments deleted from src/background/index.ts; only a
  retirement comment naming Plan 01-08 D-14-remux remains.
  EmptyVideoBufferError surface preserved (W-01 free-text rename only).
- CONTEXT.md amendment provenance verified intact (B-01 grep checks all
  pass — no file mutation by this plan; the orchestrator landed the
  amendment at plan-creation time in commit 2e499d7).
- 2 deviations documented (Rule 1: tsc-required codec field in
  EncodedVideoChunkMetadata; Rule 3: stale comment cleanup in
  decodeBufferSegments). No scope creep.
- Self-check: all 6 files + 4 task commits verified present.
- Task 5 = checkpoint:human-verify (operator regenerates
  tests/fixtures/last_30sec.webm via ./smoke.sh, confirms Chrome + mpv
  playback ~30 s, flips the 2 webm-playback duration tests GREEN).
This commit is contained in:
2026-05-17 09:29:26 +02:00
parent 35db6c2357
commit aabbd0c05c

View File

@@ -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<Blob>"
- "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<ArrayBufferTarget>.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<Blob> 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<ArrayBufferTarget>.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 `<how-to-verify>` 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 `<parallel_execution>` 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)*