Milestone v1 (v2.0.0): Mokosh — Session Capture #1
@@ -6,7 +6,8 @@
|
||||
// OFFSCREEN_READY handshake (Pattern 4).
|
||||
|
||||
import { OffscreenLogger } from '../shared/logger';
|
||||
import type { Message, VideoChunk } from '../shared/types';
|
||||
import { blobToBase64 } from '../shared/binary';
|
||||
import type { Message, TransferredVideoChunk, VideoChunk } from '../shared/types';
|
||||
|
||||
// ─── Константы (per CON-video-codec, CON-video-window) ──────────────────
|
||||
export const VIDEO_BUFFER_DURATION_MS = 30_000; // 30 секунд
|
||||
@@ -172,13 +173,57 @@ function onPortMessage(message: unknown): void {
|
||||
}
|
||||
const type = (message as { type?: unknown }).type;
|
||||
if (type === 'REQUEST_BUFFER') {
|
||||
if (keepalivePort !== null) {
|
||||
keepalivePort.postMessage({ type: 'BUFFER', chunks: getBuffer() });
|
||||
}
|
||||
// Fire-and-forget: the chrome.runtime.Port.onMessage listener API
|
||||
// ignores the return value. We do the async base64 encoding inside
|
||||
// an IIFE so the listener stays synchronous-typed (void return).
|
||||
//
|
||||
// D-12 fix: each chunk's Blob is encoded to base64 BEFORE postMessage
|
||||
// because chrome.runtime.Port JSON-serializes across extension contexts,
|
||||
// and JSON.stringify(blob) === "{}". See src/shared/binary.ts and
|
||||
// tests/offscreen/port-serialization.test.ts for the contract.
|
||||
void encodeAndSendBuffer();
|
||||
}
|
||||
// Any unknown port message type is silently dropped (T-1-04 defense-in-depth).
|
||||
}
|
||||
|
||||
async function encodeAndSendBuffer(): Promise<void> {
|
||||
const chunks = getBuffer();
|
||||
// Per-chunk defensive encode: if a single Blob fails to encode (e.g.
|
||||
// an unexpected ArrayBuffer detach mid-flight), log the error and skip
|
||||
// it rather than dropping the entire BUFFER response. Operators get
|
||||
// partial video > no video.
|
||||
const encodeResults = await Promise.all(
|
||||
chunks.map(async (chunk): Promise<TransferredVideoChunk | null> => {
|
||||
try {
|
||||
const data = await blobToBase64(chunk.data);
|
||||
return {
|
||||
data,
|
||||
type: chunk.data.type,
|
||||
timestamp: chunk.timestamp,
|
||||
isFirst: chunk.isFirst,
|
||||
};
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
'blobToBase64 failed; skipping chunk',
|
||||
'timestamp:', chunk.timestamp,
|
||||
'isFirst:', chunk.isFirst,
|
||||
'error:', err,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
);
|
||||
const transferred: TransferredVideoChunk[] = encodeResults.filter(
|
||||
(c): c is TransferredVideoChunk => 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 });
|
||||
}
|
||||
|
||||
function connectPort(): void {
|
||||
teardownPortTimers();
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user