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