Milestone v1 (v2.0.0): Mokosh — Session Capture #1
@@ -6,7 +6,8 @@
|
|||||||
// OFFSCREEN_READY handshake (Pattern 4).
|
// OFFSCREEN_READY handshake (Pattern 4).
|
||||||
|
|
||||||
import { OffscreenLogger } from '../shared/logger';
|
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) ──────────────────
|
// ─── Константы (per CON-video-codec, CON-video-window) ──────────────────
|
||||||
export const VIDEO_BUFFER_DURATION_MS = 30_000; // 30 секунд
|
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;
|
const type = (message as { type?: unknown }).type;
|
||||||
if (type === 'REQUEST_BUFFER') {
|
if (type === 'REQUEST_BUFFER') {
|
||||||
if (keepalivePort !== null) {
|
// Fire-and-forget: the chrome.runtime.Port.onMessage listener API
|
||||||
keepalivePort.postMessage({ type: 'BUFFER', chunks: getBuffer() });
|
// 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).
|
// 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 {
|
function connectPort(): void {
|
||||||
teardownPortTimers();
|
teardownPortTimers();
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user