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

18 KiB
Raw Permalink Blame History

phase, plan, subsystem, tags, requires, provides, affects, tech-stack, key-files, key-decisions, patterns-established, requirements-completed, duration, completed
phase plan subsystem tags requires provides affects tech-stack key-files key-decisions patterns-established requirements-completed duration completed
01-stabilize-video-pipeline 08 video-pipeline
webm
remux
ts-ebml
webm-muxer
vp9
matroska
ebml
service-worker
phase provides
01-stabilize-video-pipeline 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 provides
01-stabilize-video-pipeline 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.
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
phase note
04-spec-smoke-verification SPEC §10 #7 (last_30sec.webm plays in browser) is now functionally satisfied at the codebase level pending the Task 5 operator checkpoint.
added patterns
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)
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.
created modified
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
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
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).
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.
REQ-video-ring-buffer
6min 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 retiredmergeVideoSegments() deleted from src/background/index.ts; only a retirement comment naming Plan 01-08 D-14-remux remains.
  • Single-EBML WebM remux landedsrc/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 pinnedtests/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-compat5035314 (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 invariants407e683 (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 — GREEN41e94d5 (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 amendment35db6c2 (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):

# 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:

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)