From 670daa3fe845f4363104e2a773ca3385d585efb1 Mon Sep 17 00:00:00 2001 From: Mark Date: Fri, 15 May 2026 21:12:46 +0200 Subject: [PATCH] feat(fix-a3): adapt SW receive path to segment semantics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to the D-13 activation in src/offscreen/recorder.ts. Each entry on the BUFFER port message is now a self-contained WebM segment (not a partial chunk), so the SW-side concat is trivial: sort by timestamp and Blob-concatenate. The resulting multi-EBML- header file plays natively in Chrome (SPEC §10 #7 scope). Changes in src/background/index.ts: - mergeVideoChunks renamed to mergeVideoSegments; doc comment and log lines say "segment" throughout. No behavioural change beyond removing the now-stale per-chunk `isFirst` logging. - getVideoBufferFromOffscreen no longer reads or carries the `isFirst` field when decoding wire payload into VideoChunk — D-13's segment lifecycle makes the flag meaningless (every segment is a fresh recorder boundary, every segment's first chunk is implicitly the header). The field stays optional on VideoChunk for one more commit; commit 4 sweeps the type rename and drops it. - The single mergeVideoChunks call site in createArchive updated to the new name. Verification: - npx vitest run → 28 passed / 2 failed (same 2 empirical-ffmpeg REDs from webm-playback.test.ts; unchanged from prior commit — the fixture is still the stale single-continuous-recorder one). - npx tsc --noEmit clean. - src/background/index.ts now has zero references to addChunk / trimAged / firstChunkSaved / isFirst. --- src/background/index.ts | 66 ++++++++++++++++++++++++++--------------- 1 file changed, 42 insertions(+), 24 deletions(-) diff --git a/src/background/index.ts b/src/background/index.ts index 3702657..0521cda 100644 --- a/src/background/index.ts +++ b/src/background/index.ts @@ -122,26 +122,27 @@ async function getVideoBufferFromOffscreen(): Promise { ) { clearTimeout(timer); port.onMessage.removeListener(handler); - // D-12 fix: chunks arrive as TransferredVideoChunk[] (base64 - // string + MIME). Decode each back into a VideoChunk so - // mergeVideoChunks keeps operating on real Blobs. See - // src/shared/binary.ts and the GREEN block of - // tests/offscreen/port-serialization.test.ts. - const wireChunks = + // D-12 wire format: payload arrives as TransferredVideoChunk[] + // — base64 string + MIME — because chrome.runtime.Port + // JSON-serializes across extension contexts. Decode each entry + // back into a VideoChunk. D-13 lifecycle: each entry is now a + // self-contained WebM segment (~10 s, EBML header + seed + // keyframe), not a partial chunk; concatenating them + // sequentially produces a multi-EBML-header file Chrome plays + // natively. See src/shared/binary.ts + RESEARCH.md Pattern 3. + const wireSegments = (msg as { chunks?: TransferredVideoChunk[] }).chunks ?? []; const chunks: VideoChunk[] = []; - for (const wire of wireChunks) { + for (const wire of wireSegments) { try { chunks.push({ data: base64ToBlob(wire.data, wire.type || VIDEO_MIME_FALLBACK), timestamp: wire.timestamp, - isFirst: wire.isFirst, }); } catch (err) { logger.warn( - 'base64ToBlob failed; skipping chunk', + 'base64ToBlob failed; skipping segment', 'timestamp:', wire.timestamp, - 'isFirst:', wire.isFirst, 'error:', err, ); } @@ -235,23 +236,40 @@ async function captureScreenshot(): Promise { return cachedScreenshot; } -// Склейка видео чанков -function mergeVideoChunks(chunks: VideoChunk[]): Blob { - logger.log(`Merging ${chunks.length} chunks`); +// Склейка сегментов в один WebM-файл. +// +// Под D-13 каждый VideoChunk — это самодостаточный ~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: VideoChunk[]): Blob { + logger.log(`Merging ${segments.length} segments`); - // Сортируем по времени, чтобы сохранить правильный порядок - const sortedChunks = [...chunks].sort((a, b) => a.timestamp - b.timestamp); + // Сортируем по времени, чтобы сохранить правильный порядок (старшие + // сегменты — раньше). Под D-13 порядок уже задаётся offscreen-стороной, + // но сортируем оборонительно — стоимость copy+sort ничтожна. + const sortedSegments = [...segments].sort((a, b) => a.timestamp - b.timestamp); - logger.log(`Chunks sorted, first timestamp: ${sortedChunks[0]?.timestamp}, last: ${sortedChunks[sortedChunks.length - 1]?.timestamp}`); + logger.log( + `Segments sorted, first timestamp: ${sortedSegments[0]?.timestamp}, ` + + `last: ${sortedSegments[sortedSegments.length - 1]?.timestamp}`, + ); - // Конвертируем в массив Blob - const blobs: Blob[] = sortedChunks.map((chunk, index) => { - logger.log(`Adding chunk ${index}, size: ${chunk.data.size} bytes, isFirst: ${chunk.isFirst}`); - return chunk.data; + // Каждый сегмент уже валидный 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 chunks merged: ${blobs.length}`); + logger.log( + `Final video blob size: ${finalBlob.size} bytes, ` + + `total segments merged: ${blobs.length}`, + ); return finalBlob; } @@ -268,13 +286,13 @@ async function createArchive( const zip = new JSZip(); - // Добавляем видео + // Добавляем видео (D-13: чанки в ответе — это уже сегменты) if (videoBufferResponse.chunks.length > 0) { - const videoBlob = mergeVideoChunks(videoBufferResponse.chunks); + const videoBlob = mergeVideoSegments(videoBufferResponse.chunks); zip.file('video/last_30sec.webm', videoBlob); logger.log(`✓ Added video: ${videoBlob.size} bytes`); } else { - logger.warn('✗ No video chunks to add'); + logger.warn('✗ No video segments to add'); } // Добавляем rrweb события