diff --git a/src/background/index.ts b/src/background/index.ts index 21b184f..64722bb 100644 --- a/src/background/index.ts +++ b/src/background/index.ts @@ -16,6 +16,20 @@ const VIDEO_MIME_FALLBACK = 'video/webm;codecs=vp9'; const logger = new Logger('Main'); +// Option C: a typed error so saveArchive can distinguish the empty-video +// failure (operator-facing — shows a clear RECORDING_ERROR popup) from +// generic createArchive failures (zip/manifest/etc.). The 'empty-video- +// buffer' code joins the CaptureErrorCode union surfaced by the +// offscreen recorder (see src/offscreen/recorder.ts: classifyCaptureError) +// — same operator-facing vocabulary, different production point. +export class EmptyVideoBufferError extends Error { + readonly code = 'empty-video-buffer' as const; + constructor(detail: string) { + super(`empty-video-buffer: ${detail}`); + this.name = 'EmptyVideoBufferError'; + } +} + // Состояние // Видеобуфер живёт в offscreen-документе (D-16). SW не хранит чанки локально: // при экспорте он спрашивает буфер у offscreen через long-lived port (D-17). @@ -418,14 +432,28 @@ async function createArchive( const zip = new JSZip(); - // Добавляем видео (D-13: каждая запись — самодостаточный WebM-сегмент) - if (videoBufferResponse.segments.length > 0) { - const videoBlob = mergeVideoSegments(videoBufferResponse.segments); - zip.file('video/last_30sec.webm', videoBlob); - logger.log(`✓ Added video: ${videoBlob.size} bytes`); - } else { - logger.warn('✗ No video segments to add'); + // Добавляем видео (D-13: каждая запись — самодостаточный WebM-сегмент). + // + // Option C (debug session empty-archive-port-race): the upstream + // silent-skip branch is GONE. Shipping a zip with no video defeats the + // entire purpose of the operator-save flow (Phase 1 goal); the operator + // must see a clear failure instead of receiving a 88 KB archive with no + // last_30sec.webm. saveArchive's catch translates the throw into the + // {success: false, error: 'empty-video-buffer'} response shape the + // popup already handles via the RECORDING_ERROR surface. + if (videoBufferResponse.segments.length === 0) { + throw new EmptyVideoBufferError( + 'no video segments available — buffer fetch returned empty (port replacement timed out, or recorder never started)', + ); } + const videoBlob = mergeVideoSegments(videoBufferResponse.segments); + if (videoBlob.size === 0) { + throw new EmptyVideoBufferError( + `merged video blob is zero bytes (segment count=${videoBufferResponse.segments.length})`, + ); + } + zip.file('video/last_30sec.webm', videoBlob); + logger.log(`✓ Added video: ${videoBlob.size} bytes`); // Добавляем rrweb события const rrwebJson = JSON.stringify(rrwebEvents, null, 2); @@ -573,6 +601,23 @@ async function saveArchive() { } catch (error) { logger.error('Failed to save archive:', error); + // Option C: the empty-video failure is operator-visible. Emit + // RECORDING_ERROR so the popup's existing handler can surface it + // (same channel codec-unsupported, user-cancelled, etc. ride). + // Other createArchive failures (zip libs, JSZip internals) stay + // SW-side only — they're not actionable by the operator. + if (error instanceof EmptyVideoBufferError) { + try { + chrome.runtime.sendMessage({ + type: 'RECORDING_ERROR', + error: error.code, + }); + } catch (sendErr) { + // Best-effort notification — if the popup is closed we have + // nothing else to do. + logger.warn('Failed to broadcast RECORDING_ERROR:', sendErr); + } + } return { success: false, error }; } }