feat(01-08): swap mergeVideoSegments -> await remuxSegments at call site

- 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.
This commit is contained in:
2026-05-17 09:27:45 +02:00
parent 41e94d5daa
commit 35db6c2357

View File

@@ -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<Blob> {
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);