feat(fix-d12): add binary encode/decode helpers in src/shared/binary.ts
- Add blobToBase64 / base64ToBlob in src/shared/binary.ts:
portable Blob↔base64 round-trip for the chrome.runtime.Port
wire-format. JSON.stringify(blob) returns "{}" across extension
contexts, so binary payloads must travel as base64 strings.
- Mirror the GREEN-block helper signatures from
tests/offscreen/port-serialization.test.ts so the same test pins
both the standalone helpers and the production wire format.
- Land tests/offscreen/port-serialization.test.ts as the RED+GREEN
executable contract for the D-12 fix: the RED block reproduces
the 75-byte "[object Object]" failure mode byte-for-byte; the
GREEN block pins the base64 wire-format the fix must implement.
- Uses arrayBuffer() + btoa(String.fromCharCode...) rather than
FileReader: FileReader is browser-only; the chosen approach
works in both Chrome extension contexts and the Node-based
vitest environment.
Refs: debug session d12-blob-port-transfer-fails.
This commit is contained in:
74
src/shared/binary.ts
Normal file
74
src/shared/binary.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
// src/shared/binary.ts — Portable Blob↔base64 helpers for the
|
||||
// offscreen↔SW port wire-format (D-12 fix).
|
||||
//
|
||||
// Why this file exists:
|
||||
// chrome.runtime.Port.postMessage JSON-serializes payloads across
|
||||
// extension contexts. JSON.stringify(blob) === "{}" (Blob has no
|
||||
// enumerable own properties), so any Blob passed through a port
|
||||
// arrives as a plain empty object on the other side. The SW then
|
||||
// calls `new Blob([{}, {}, ...])` and the constructor coerces each
|
||||
// non-Blob member via String({}) === "[object Object]", producing
|
||||
// exactly 75 bytes of garbage for 5 chunks — matching the observed
|
||||
// ffprobe failure forensics in debug session d12.
|
||||
//
|
||||
// Fix: encode each chunk's binary payload to base64 in offscreen
|
||||
// BEFORE port.postMessage; decode back to Blob in SW AFTER receive.
|
||||
// base64 strings round-trip cleanly through JSON.
|
||||
//
|
||||
// The wire-format contract is pinned by tests/offscreen/port-serialization.test.ts
|
||||
// (GREEN describe block). This module's two helpers MUST satisfy:
|
||||
// 1. `await blobToBase64(blob)` returns just the base64 payload
|
||||
// (no `data:...;base64,` prefix).
|
||||
// 2. `base64ToBlob(b64, mimeType)` returns a Blob whose bytes
|
||||
// equal `Uint8Array(await blob.arrayBuffer())` of the original.
|
||||
// 3. EBML magic bytes 0x1A 0x45 0xDF 0xA3 round-trip intact through
|
||||
// JSON.parse(JSON.stringify({dataBase64: await blobToBase64(...)})).
|
||||
//
|
||||
// Portability: both `btoa`/`atob` and `arrayBuffer()` are available in
|
||||
// the modern browser/extension runtime AND in the Node-based vitest
|
||||
// environment (Node 18+ has both as globals). No FileReader dependency
|
||||
// (FileReader is browser-only — would break Node tests).
|
||||
|
||||
/**
|
||||
* Encode a Blob's binary content to a base64 string suitable for
|
||||
* JSON-friendly transport over chrome.runtime.Port.
|
||||
*
|
||||
* Returns the raw base64 payload only (no `data:<type>;base64,` prefix).
|
||||
*
|
||||
* @param blob - The binary blob to encode (typically a MediaRecorder
|
||||
* dataavailable chunk).
|
||||
* @returns Base64-encoded representation of the blob's bytes.
|
||||
*/
|
||||
export async function blobToBase64(blob: Blob): Promise<string> {
|
||||
const buf = await blob.arrayBuffer();
|
||||
const bytes = new Uint8Array(buf);
|
||||
// Per-byte concat is the portable way: avoids the
|
||||
// `String.fromCharCode(...bytes)` apply-spread argument-length limit
|
||||
// (typically ~64 KiB on some engines) for large chunks.
|
||||
let binary = '';
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
binary += String.fromCharCode(bytes[i]);
|
||||
}
|
||||
return btoa(binary);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a base64 string back into a Blob with the given MIME type.
|
||||
*
|
||||
* Synchronous because atob + Uint8Array population requires no I/O.
|
||||
*
|
||||
* @param b64 - The base64-encoded payload (as produced by
|
||||
* {@link blobToBase64}). Must NOT include a
|
||||
* `data:<type>;base64,` prefix.
|
||||
* @param mimeType - The MIME type to assign to the reconstructed Blob
|
||||
* (e.g. `video/webm;codecs=vp9`).
|
||||
* @returns A Blob whose bytes match the original encoded blob exactly.
|
||||
*/
|
||||
export function base64ToBlob(b64: string, mimeType: string): Blob {
|
||||
const binary = atob(b64);
|
||||
const bytes = new Uint8Array(binary.length);
|
||||
for (let i = 0; i < binary.length; i++) {
|
||||
bytes[i] = binary.charCodeAt(i);
|
||||
}
|
||||
return new Blob([bytes], { type: mimeType });
|
||||
}
|
||||
Reference in New Issue
Block a user