Files
mokosh/.planning/phases/01-stabilize-video-pipeline/01-08-SUMMARY.md
Mark aabbd0c05c 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).
2026-05-17 09:29:26 +02:00

211 lines
18 KiB
Markdown
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.
---
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)*