feat(fix-d12): decode chunks from base64 in SW BUFFER receive

- 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.
This commit is contained in:
2026-05-15 20:18:31 +02:00
parent 283184978f
commit d5bb948d95

View File

@@ -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<VideoBufferResponse> {
@@ -110,7 +122,30 @@ async function getVideoBufferFromOffscreen(): Promise<VideoBufferResponse> {
) {
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 });
}
};