fix(01-review): WR-07 base64ToBlob empty-input shortcut + SW-side empty-segment filter

This commit is contained in:
2026-05-16 10:24:38 +02:00
parent 349ae88a8e
commit e9aae09f6d
3 changed files with 81 additions and 7 deletions

View File

@@ -153,21 +153,43 @@ async function getVideoBufferFromOffscreen(): Promise<VideoBufferResponse> {
// RESEARCH.md Pattern 3.
const wireSegments =
(msg as { segments?: TransferredVideoSegment[] }).segments ?? [];
const segments: VideoSegment[] = [];
for (const wire of wireSegments) {
// WR-07 fix: filter empty wire segments BEFORE base64 decode.
// An empty wire.data would decode to a zero-byte Blob; the
// SW-side mergeVideoSegments would then concat it into the
// output WebM, producing a stray empty EBML segment that
// breaks Chrome playback. We split into two passes (filter →
// decode → filter-non-empty) so the iteration semantics stay
// declarative (no early-return in the loop body).
const nonEmptyWires = wireSegments.filter((wire) => {
const isEmpty = !wire.data || wire.data.length === 0;
if (isEmpty) {
logger.warn(
'Skipping empty wire segment (zero-length base64)',
'timestamp:', wire.timestamp,
);
}
return !isEmpty;
});
const segments: VideoSegment[] = nonEmptyWires.flatMap((wire) => {
try {
segments.push({
data: base64ToBlob(wire.data, wire.type || VIDEO_MIME_FALLBACK),
timestamp: wire.timestamp,
});
const blob = base64ToBlob(wire.data, wire.type || VIDEO_MIME_FALLBACK);
if (blob.size === 0) {
logger.warn(
'Skipping segment that decoded to zero bytes',
'timestamp:', wire.timestamp,
);
return [];
}
return [{ data: blob, timestamp: wire.timestamp }];
} catch (err) {
logger.warn(
'base64ToBlob failed; skipping segment',
'timestamp:', wire.timestamp,
'error:', err,
);
return [];
}
}
});
resolve({ segments });
}
};

View File

@@ -65,6 +65,17 @@ export async function blobToBase64(blob: Blob): Promise<string> {
* @returns A Blob whose bytes match the original encoded blob exactly.
*/
export function base64ToBlob(b64: string, mimeType: string): Blob {
// WR-07 fix: defensive early-return for empty input. `atob('')` returns
// an empty string and the resulting Blob has size 0 — currently no
// caller filters zero-size segments, which would corrupt the
// concatenated WebM with a stray empty EBML segment. Return an
// explicit empty Blob here so the SW-side filter on
// `segment.data.size > 0` (see src/background/index.ts
// getVideoBufferFromOffscreen) can pre-filter cleanly without an
// extra atob round-trip.
if (b64.length === 0) {
return new Blob([], { type: mimeType });
}
const binary = atob(b64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {