---
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)*
