diff --git a/src/background/index.ts b/src/background/index.ts index 0521cda..94662ed 100644 --- a/src/background/index.ts +++ b/src/background/index.ts @@ -2,8 +2,8 @@ import { Logger } from '../shared/logger'; import { base64ToBlob } from '../shared/binary'; import type { Message, - TransferredVideoChunk, - VideoChunk, + TransferredVideoSegment, + VideoSegment, SessionMetadata, VideoBufferResponse } from '../shared/types'; @@ -105,14 +105,14 @@ const BUFFER_FETCH_TIMEOUT_MS = 2_000; async function getVideoBufferFromOffscreen(): Promise { if (videoPort === null) { logger.warn('No offscreen port available; returning empty buffer'); - return { chunks: [] }; + return { segments: [] }; } const port = videoPort; return new Promise((resolve) => { const timer = setTimeout(() => { port.onMessage.removeListener(handler); logger.warn(`Buffer fetch timed out after ${BUFFER_FETCH_TIMEOUT_MS} ms`); - resolve({ chunks: [] }); + resolve({ segments: [] }); }, BUFFER_FETCH_TIMEOUT_MS); const handler = (msg: unknown) => { if ( @@ -122,20 +122,19 @@ async function getVideoBufferFromOffscreen(): Promise { ) { clearTimeout(timer); port.onMessage.removeListener(handler); - // 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. + // D-12 wire format + D-13 segment lifecycle: payload arrives + // as TransferredVideoSegment[] (base64 string + MIME). Decode + // each entry back into a VideoSegment — each is a + // self-contained ~10 s WebM (EBML header + seed keyframe). + // 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[] = []; + (msg as { segments?: TransferredVideoSegment[] }).segments ?? []; + const segments: VideoSegment[] = []; for (const wire of wireSegments) { try { - chunks.push({ + segments.push({ data: base64ToBlob(wire.data, wire.type || VIDEO_MIME_FALLBACK), timestamp: wire.timestamp, }); @@ -147,7 +146,7 @@ async function getVideoBufferFromOffscreen(): Promise { ); } } - resolve({ chunks }); + resolve({ segments }); } }; port.onMessage.addListener(handler); @@ -238,7 +237,7 @@ async function captureScreenshot(): Promise { // Склейка сегментов в один WebM-файл. // -// Под D-13 каждый VideoChunk — это самодостаточный ~10-секундный WebM +// Под D-13 каждый VideoSegment — это самодостаточный ~10-секундный WebM // (собственный EBML-заголовок + seed keyframe). Сортируем по timestamp // и склеиваем подряд: получаем multi-EBML-header файл, который Chrome // проигрывает последовательно (см. SPEC §10 #7 — требуется проигрывание @@ -246,7 +245,7 @@ async function captureScreenshot(): Promise { // // Никакой логики «pin первого чанка» или header-retention больше нет // — это снято вместе с D-09..D-11 и активацией D-13 в recorder.ts. -function mergeVideoSegments(segments: VideoChunk[]): Blob { +function mergeVideoSegments(segments: VideoSegment[]): Blob { logger.log(`Merging ${segments.length} segments`); // Сортируем по времени, чтобы сохранить правильный порядок (старшие @@ -286,9 +285,9 @@ async function createArchive( const zip = new JSZip(); - // Добавляем видео (D-13: чанки в ответе — это уже сегменты) - if (videoBufferResponse.chunks.length > 0) { - const videoBlob = mergeVideoSegments(videoBufferResponse.chunks); + // Добавляем видео (D-13: каждая запись — самодостаточный WebM-сегмент) + if (videoBufferResponse.segments.length > 0) { + const videoBlob = mergeVideoSegments(videoBufferResponse.segments); zip.file('video/last_30sec.webm', videoBlob); logger.log(`✓ Added video: ${videoBlob.size} bytes`); } else { @@ -386,7 +385,7 @@ async function saveArchive() { // Получаем видео буфер из offscreen через long-lived port (D-17) const videoBufferResp = await getVideoBufferFromOffscreen(); - logger.log(`Video buffer: ${videoBufferResp.chunks.length} chunks`); + logger.log(`Video buffer: ${videoBufferResp.segments.length} segments`); // Получаем rrweb события от content script logger.log(`Requesting rrweb events from content script on tab ${tab.id} (${tab.url})...`); diff --git a/src/offscreen/recorder.ts b/src/offscreen/recorder.ts index 8db1b8f..6c59881 100644 --- a/src/offscreen/recorder.ts +++ b/src/offscreen/recorder.ts @@ -16,7 +16,7 @@ import { OffscreenLogger } from '../shared/logger'; import { blobToBase64 } from '../shared/binary'; -import type { Message, TransferredVideoChunk } from '../shared/types'; +import type { Message, TransferredVideoSegment } from '../shared/types'; // ─── Константы (per CON-video-codec, CON-video-window, D-13) ──────────── // Длительность одного сегмента — 10 с (D-13, RESEARCH.md Pattern 3). @@ -355,7 +355,7 @@ async function encodeAndSendBuffer(): Promise { // (например, неожиданный detach ArrayBuffer-а на лету), логируем и // пропускаем — частичное видео > отсутствующее видео. const encodeResults = await Promise.all( - allSegments.map(async (segment, idx): Promise => { + allSegments.map(async (segment, idx): Promise => { try { const data = await blobToBase64(segment); return { @@ -376,15 +376,15 @@ async function encodeAndSendBuffer(): Promise { } }), ); - const transferred: TransferredVideoChunk[] = encodeResults.filter( - (c): c is TransferredVideoChunk => c !== null, + const transferred: TransferredVideoSegment[] = encodeResults.filter( + (c): c is TransferredVideoSegment => c !== null, ); // Re-check port AFTER the await: it may have disconnected during encoding. if (keepalivePort === null) { logger.warn('port disconnected during base64 encoding; dropping BUFFER response'); return; } - keepalivePort.postMessage({ type: 'BUFFER', chunks: transferred }); + keepalivePort.postMessage({ type: 'BUFFER', segments: transferred }); } function connectPort(): void { diff --git a/src/shared/types.ts b/src/shared/types.ts index e5f4856..94f9ca0 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -21,7 +21,7 @@ export interface Message { tabId?: number; } -// Типы сообщений в long-lived port (offscreen ↔ SW; D-17 / Plan 04) +// Типы сообщений в long-lived port (offscreen ↔ SW; D-17 / Plan 04 / D-13) export type PortMessageType = | 'PING' | 'REQUEST_BUFFER' @@ -29,31 +29,35 @@ export type PortMessageType = export interface PortMessage { type: PortMessageType; - // Wire-format (post-D-12 fix): chunks travel as TransferredVideoChunk[] - // because chrome.runtime.Port JSON-serializes payloads, and - // JSON.stringify(blob) === "{}" loses binary content. The receive - // side reconstructs VideoChunk[] via src/shared/binary.ts. - chunks?: TransferredVideoChunk[]; + // Wire-format (D-12 base64 transfer + D-13 segment lifecycle): + // segments travel as TransferredVideoSegment[] because + // chrome.runtime.Port JSON-serializes payloads across extension + // contexts and JSON.stringify(blob) === "{}" loses binary content. + // Each entry is one self-contained ~10 s WebM segment (EBML header + // + seed keyframe). The receive side reconstructs VideoSegment[] + // via src/shared/binary.ts. + segments?: TransferredVideoSegment[]; } -// In-memory chunk shape used by the offscreen ring buffer and by -// mergeVideoChunks after the SW decodes the wire format. -export interface VideoChunk { +// In-memory segment shape used by mergeVideoSegments after the SW +// decodes the wire format. Под D-13 каждый сегмент — самодостаточный +// WebM-блок ≈ 10 секунд (свой EBML-заголовок и стартовый keyframe). +export interface VideoSegment { data: Blob; timestamp: number; - isFirst?: boolean; } -// Wire-format for video chunks traveling across the offscreen↔SW -// chrome.runtime.Port boundary. Replaces the previous VideoChunk[] -// payload, which failed because Blob is not JSON-serializable. -// See debug session d12-blob-port-transfer-fails and the GREEN block -// of tests/offscreen/port-serialization.test.ts for the pinned shape. -export interface TransferredVideoChunk { +// Wire-format for video segments traveling across the offscreen↔SW +// chrome.runtime.Port boundary. Replaces the previous Blob payload, +// which failed because Blob is not JSON-serializable. +// See debug session d12-blob-port-transfer-fails (resolved) and the +// GREEN block of tests/offscreen/port-serialization.test.ts for the +// pinned shape. The header-pin flag from the D-09..D-11 era is gone +// — under D-13 every segment IS implicitly its own header. +export interface TransferredVideoSegment { data: string; // base64-encoded blob bytes (no data: prefix) type: string; // MIME type to apply when reconstructing the Blob timestamp: number; - isFirst?: boolean; } // Лог событий пользователя @@ -86,9 +90,9 @@ export interface PopupState { // Ответы от Service Worker export interface VideoBufferResponse { - chunks: VideoChunk[]; + segments: VideoSegment[]; } export interface RrwebEventsResponse { events: any[]; -} \ No newline at end of file +}