Milestone v1 (v2.0.0): Mokosh — Session Capture #1
@@ -7,6 +7,7 @@ import type {
|
|||||||
SessionMetadata,
|
SessionMetadata,
|
||||||
VideoBufferResponse
|
VideoBufferResponse
|
||||||
} from '../shared/types';
|
} from '../shared/types';
|
||||||
|
import { remuxSegments } from './webm-remux';
|
||||||
import JSZip from 'jszip';
|
import JSZip from 'jszip';
|
||||||
|
|
||||||
// Default MIME applied when a wire chunk somehow lacks a type
|
// Default MIME applied when a wire chunk somehow lacks a type
|
||||||
@@ -130,9 +131,10 @@ function decodeBufferSegments(
|
|||||||
wireSegments: TransferredVideoSegment[],
|
wireSegments: TransferredVideoSegment[],
|
||||||
): VideoSegment[] {
|
): VideoSegment[] {
|
||||||
// WR-07 fix: filter empty wire segments BEFORE base64 decode. An empty
|
// WR-07 fix: filter empty wire segments BEFORE base64 decode. An empty
|
||||||
// wire.data would decode to a zero-byte Blob; mergeVideoSegments would
|
// wire.data would decode to a zero-byte Blob; the remux pipeline
|
||||||
// then concat it into the output WebM, producing a stray empty EBML
|
// (src/background/webm-remux.ts, Plan 01-08 D-14-remux) would try
|
||||||
// segment that breaks Chrome playback. Two passes (filter -> decode ->
|
// 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.
|
// filter-non-empty) keep the iteration semantics declarative.
|
||||||
const nonEmptyWires = wireSegments.filter((wire) => {
|
const nonEmptyWires = wireSegments.filter((wire) => {
|
||||||
const isEmpty = !wire.data || wire.data.length === 0;
|
const isEmpty = !wire.data || wire.data.length === 0;
|
||||||
@@ -382,43 +384,14 @@ async function captureScreenshot(): Promise<Blob> {
|
|||||||
return cachedScreenshot;
|
return cachedScreenshot;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Склейка сегментов в один WebM-файл.
|
// 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.
|
||||||
// Под D-13 каждый VideoSegment — это самодостаточный ~10-секундный WebM
|
// The concat-of-self-contained-WebM-segments approach produced a
|
||||||
// (собственный EBML-заголовок + seed keyframe). Сортируем по timestamp
|
// multi-EBML-header file that mpv / Chrome / ffprobe truncated to
|
||||||
// и склеиваем подряд: получаем multi-EBML-header файл, который Chrome
|
// the first segment's local Info.Duration ~9.94 s; the remux path
|
||||||
// проигрывает последовательно (см. SPEC §10 #7 — требуется проигрывание
|
// emits a single-EBML WebM whose Info.Duration covers the full ~30 s
|
||||||
// в Chrome, кросс-плейер совместимость вне scope Phase 1).
|
// timeline. D-13's recorder-side restart-segments lifecycle is
|
||||||
//
|
// preserved — only the merge step is replaced.
|
||||||
// Никакой логики «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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Создание архива
|
// Создание архива
|
||||||
async function createArchive(
|
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)',
|
'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) {
|
if (videoBlob.size === 0) {
|
||||||
throw new EmptyVideoBufferError(
|
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);
|
zip.file('video/last_30sec.webm', videoBlob);
|
||||||
logger.log(`✓ Added video: ${videoBlob.size} bytes`);
|
logger.log(`✓ Added video (remuxed): ${videoBlob.size} bytes`);
|
||||||
|
|
||||||
// Добавляем rrweb события
|
// Добавляем rrweb события
|
||||||
const rrwebJson = JSON.stringify(rrwebEvents, null, 2);
|
const rrwebJson = JSON.stringify(rrwebEvents, null, 2);
|
||||||
|
|||||||
Reference in New Issue
Block a user