- 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).
18 KiB
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 |
|
|
|
|
|
|
|
|
|
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 retired —
mergeVideoSegments()deleted fromsrc/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'sMuxer<ArrayBufferTarget>.addVideoChunkRawwith 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.tsasserts both libraries import cleanly withwindow/documentabsent onglobalThis, 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):
- 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. - 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. - 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. - 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 buildexit 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; usesLogger('Remux'); noas 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 withoutwindow/documentglobals.src/background/index.ts(modified) — addedimport { remuxSegments } from './webm-remux'; deletedmergeVideoSegmentsbody (kept retirement comment); swappedcreateArchivetoawait remuxSegments(...); renamed error detail stringmerged → remuxed; updated staledecodeBufferSegmentsWR-07 comment to reference the new pipeline.package.json+package-lock.json(modified) — addedts-ebml ^3.0.2+webm-muxer ^5.1.4todependencies.
Decisions Made
- Codec metadata fallback:
EncodedVideoChunkMetadata.decoderConfigrequires bothcodecanddescription. When a CodecPrivate is extracted (rare for MediaRecorder VP9), the implementation attachescodec: '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 olderTimecode). Confirmed empirically via segment 1 probe (Timecode: 0results returned,Timestamp: 3results 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 tracksinClusteraccordingly so childTimestampelements 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 } }— butEncodedVideoChunkMetadata.decoderConfigisVideoDecoderConfigwhich requires bothcodecanddescription. 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) alongsidedescription. 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 --noEmitexit 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:decodeBufferSegmentscarried a WR-07 fix comment referencing the retiredmergeVideoSegmentsas the downstream consumer that would mishandle empty wire segments. After deletion the reference became dangling — futuregrep mergeVideoSegmentswould 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
ebmldependency (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.mdgetDisplayMediacursor 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:
- Load Unpacked → select
dist/ - Click the extension toolbar icon (popup opens, auto-prompts for screen)
- Screen-share picker auto-accepts the "Mokosh Smoke Test" tab
- 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)
- Click the extension icon → "Сохранить отчёт об ошибке"
smoke.shdetects the newsession_report_*.zip, extractsvideo/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— FOUNDtests/background/webm-remux.test.ts— FOUNDtests/background/webm-remux-deps.test.ts— FOUNDsrc/background/index.ts(modified) — FOUNDpackage.json(modified) — FOUNDpackage-lock.json(modified) — FOUND
Commits verified in git log --oneline --all:
5035314(Task 1: deps + SW-compat test) — FOUND407e683(Task 2: 5 RED unit tests) — FOUND41e94d5(Task 3: GREEN remux implementation) — FOUND35db6c2(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)