From 35db6c235795ca88e859f47f23c62919bd6d98e8 Mon Sep 17 00:00:00 2001 From: Mark Date: Sun, 17 May 2026 09:27:45 +0200 Subject: [PATCH] feat(01-08): swap mergeVideoSegments -> await remuxSegments at call site MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - src/background/index.ts now imports remuxSegments from './webm-remux' and awaits it in createArchive instead of synchronously calling the retired file-concat mergeVideoSegments. - mergeVideoSegments function declaration deleted entirely; only a retirement comment remains naming Plan 01-08 D-14-remux as the superseding decision. - EmptyVideoBufferError throw paths preserved on (a) zero segments AND (b) zero-byte output. Error message free-text changed from "merged video blob is zero bytes" to "remuxed video blob is zero bytes"; pre-flight grep (W-01 fix from plan checker pass) confirmed no downstream consumer matches on the legacy string — request-id-protocol.test.ts asserts on error.code ('empty-video- buffer'), not the free-text message. - createArchive remains async (was already declared async); saveArchive already awaits createArchive so no upstream signature changes. - Stale comment in decodeBufferSegments referencing mergeVideoSegments updated to reflect the new remux pipeline (Rule 3: keep forward- references accurate). - CONTEXT.md amendment provenance verified intact via 4 grep checks (B-01 fix from plan checker, folded from retired Task 6): (a) D-14-remux disambiguated marker present (1 match) (b) original D-13 line preserved (1 match) (c) D-17-port-lifecycle amendment intact (1 match) (d) webm-remux.ts replaces citation present (1 match) No CONTEXT.md mutation by this task — verify-only step. - npm run build exit 0; main SW bundle 374.56 KB (108.44 KB gzipped, matches the d13 library survey's ~100 KB estimate for ts-ebml + webm-muxer combined). - Full suite: 13 files / 60 GREEN + 2 RED (webm-playback duration assertions waiting on Task 5 fixture regen). tsc exit 0. --- src/background/index.ts | 62 +++++++++++++---------------------------- 1 file changed, 19 insertions(+), 43 deletions(-) diff --git a/src/background/index.ts b/src/background/index.ts index 64722bb..383d6b2 100644 --- a/src/background/index.ts +++ b/src/background/index.ts @@ -7,6 +7,7 @@ import type { SessionMetadata, VideoBufferResponse } from '../shared/types'; +import { remuxSegments } from './webm-remux'; import JSZip from 'jszip'; // Default MIME applied when a wire chunk somehow lacks a type @@ -130,9 +131,10 @@ function decodeBufferSegments( wireSegments: TransferredVideoSegment[], ): VideoSegment[] { // WR-07 fix: filter empty wire segments BEFORE base64 decode. An empty - // wire.data would decode to a zero-byte Blob; mergeVideoSegments would - // then concat it into the output WebM, producing a stray empty EBML - // segment that breaks Chrome playback. Two passes (filter -> decode -> + // wire.data would decode to a zero-byte Blob; the remux pipeline + // (src/background/webm-remux.ts, Plan 01-08 D-14-remux) would try + // to parse it via ts-ebml and either fail loudly or emit zero frames, + // either way wasting a parse cycle. Two passes (filter -> decode -> // filter-non-empty) keep the iteration semantics declarative. const nonEmptyWires = wireSegments.filter((wire) => { const isEmpty = !wire.data || wire.data.length === 0; @@ -382,43 +384,14 @@ async function captureScreenshot(): Promise { return cachedScreenshot; } -// Склейка сегментов в один WebM-файл. -// -// Под D-13 каждый VideoSegment — это самодостаточный ~10-секундный WebM -// (собственный EBML-заголовок + seed keyframe). Сортируем по timestamp -// и склеиваем подряд: получаем multi-EBML-header файл, который Chrome -// проигрывает последовательно (см. SPEC §10 #7 — требуется проигрывание -// в Chrome, кросс-плейер совместимость вне scope Phase 1). -// -// Никакой логики «pin первого чанка» или header-retention больше нет -// — это снято вместе с D-09..D-11 и активацией D-13 в recorder.ts. -function mergeVideoSegments(segments: VideoSegment[]): Blob { - logger.log(`Merging ${segments.length} segments`); - - // Сортируем по времени, чтобы сохранить правильный порядок (старшие - // сегменты — раньше). Под D-13 порядок уже задаётся offscreen-стороной, - // но сортируем оборонительно — стоимость copy+sort ничтожна. - const sortedSegments = [...segments].sort((a, b) => a.timestamp - b.timestamp); - - logger.log( - `Segments sorted, first timestamp: ${sortedSegments[0]?.timestamp}, ` + - `last: ${sortedSegments[sortedSegments.length - 1]?.timestamp}`, - ); - - // Каждый сегмент уже валидный WebM — конкатенация безопасна. - const blobs: Blob[] = sortedSegments.map((segment, index) => { - logger.log(`Adding segment ${index}, size: ${segment.data.size} bytes`); - return segment.data; - }); - - const finalBlob = new Blob(blobs, { type: 'video/webm' }); - logger.log( - `Final video blob size: ${finalBlob.size} bytes, ` + - `total segments merged: ${blobs.length}`, - ); - - return finalBlob; -} +// mergeVideoSegments (D-13 file-concat) retired in Plan 01-08 (D-14-remux): +// see src/background/webm-remux.ts for the single-EBML remux path. +// The concat-of-self-contained-WebM-segments approach produced a +// multi-EBML-header file that mpv / Chrome / ffprobe truncated to +// the first segment's local Info.Duration ~9.94 s; the remux path +// emits a single-EBML WebM whose Info.Duration covers the full ~30 s +// timeline. D-13's recorder-side restart-segments lifecycle is +// preserved — only the merge step is replaced. // Создание архива async function createArchive( @@ -446,14 +419,17 @@ async function createArchive( 'no video segments available — buffer fetch returned empty (port replacement timed out, or recorder never started)', ); } - const videoBlob = mergeVideoSegments(videoBufferResponse.segments); + // Plan 01-08 D-14-remux: replaces the retired mergeVideoSegments() + // file-concat with the new single-EBML WebM remux. Async now — + // ts-ebml parse + webm-muxer write happen on the SW thread. + const videoBlob = await remuxSegments(videoBufferResponse.segments); if (videoBlob.size === 0) { throw new EmptyVideoBufferError( - `merged video blob is zero bytes (segment count=${videoBufferResponse.segments.length})`, + `remuxed video blob is zero bytes (segment count=${videoBufferResponse.segments.length})`, ); } zip.file('video/last_30sec.webm', videoBlob); - logger.log(`✓ Added video: ${videoBlob.size} bytes`); + logger.log(`✓ Added video (remuxed): ${videoBlob.size} bytes`); // Добавляем rrweb события const rrwebJson = JSON.stringify(rrwebEvents, null, 2);