From d5bb948d95595c067d7ca07d6be567e333fdfcd4 Mon Sep 17 00:00:00 2001 From: Mark Date: Fri, 15 May 2026 20:18:31 +0200 Subject: [PATCH] feat(fix-d12): decode chunks from base64 in SW BUFFER receive MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Read incoming port.chunks as TransferredVideoChunk[] (was VideoChunk[] — but that was a lie because Blob doesn't survive JSON serialization across the port boundary). - Decode each wire chunk via base64ToBlob(wire.data, wire.type) and resolve VideoBufferResponse with the resulting VideoChunk[]. The existing mergeVideoChunks downstream sees real Blobs and produces a real WebM-prefixed merged blob. - Defensive per-chunk decode: log + skip individual decode failures rather than blowing up the whole fetch. Falls back to video/webm;codecs=vp9 if the wire chunk somehow omits the type (defense-in-depth — the offscreen always populates it). - Document the 2 s BUFFER_FETCH_TIMEOUT_MS budget: covers worst-case encode + post-message + JSON parse with > 1.5 s of headroom for the current 15-chunk × 100 KB sizing. Refs: debug session d12-blob-port-transfer-fails, D-17 port lifecycle. --- src/background/index.ts | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/src/background/index.ts b/src/background/index.ts index 5e3f19b..3702657 100644 --- a/src/background/index.ts +++ b/src/background/index.ts @@ -1,12 +1,19 @@ import { Logger } from '../shared/logger'; +import { base64ToBlob } from '../shared/binary'; import type { Message, + TransferredVideoChunk, VideoChunk, SessionMetadata, VideoBufferResponse } from '../shared/types'; import JSZip from 'jszip'; +// Default MIME applied when a wire chunk somehow lacks a type +// field (defense-in-depth: in normal operation the offscreen recorder +// always populates it from chunk.data.type). Matches D-20 strict codec. +const VIDEO_MIME_FALLBACK = 'video/webm;codecs=vp9'; + const logger = new Logger('Main'); // Состояние @@ -88,6 +95,11 @@ chrome.runtime.onConnect.addListener((port) => { // per-request listener installed in getVideoBufferFromOffscreen). }); +// 2 s budget covers the worst-case round-trip: offscreen base64-encodes +// up to ~15 chunks of ~100 KB each (~1.5 MB raw → ~2 MB base64) in +// well under 100 ms, post-message + JSON parse adds < 50 ms, leaving +// plenty of headroom. Bumping later is cheap if real-world recordings +// produce significantly larger buffers; today this is sufficient. const BUFFER_FETCH_TIMEOUT_MS = 2_000; async function getVideoBufferFromOffscreen(): Promise { @@ -110,7 +122,30 @@ async function getVideoBufferFromOffscreen(): Promise { ) { clearTimeout(timer); port.onMessage.removeListener(handler); - const chunks = (msg as { chunks?: VideoChunk[] }).chunks ?? []; + // 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 = + (msg as { chunks?: TransferredVideoChunk[] }).chunks ?? []; + const chunks: VideoChunk[] = []; + for (const wire of wireChunks) { + 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', + 'timestamp:', wire.timestamp, + 'isFirst:', wire.isFirst, + 'error:', err, + ); + } + } resolve({ chunks }); } };