feat(01-03): add OffscreenLogger and clean up shared types
- Add PortMessageType and PortMessage interface to src/shared/types.ts for the long-lived port (offscreen ↔ SW; D-17 / Plan 04 wires the ping loop + REQUEST_BUFFER / BUFFER traffic). - Remove 'VIDEO_CHUNK' and 'VIDEO_CHUNK_SAVED' from MessageType union (per D-19 — chunks no longer travel via chrome.runtime.sendMessage; the IndexedDB SW-side plumbing is the audit P0 #2 broken path). - OffscreenLogger class was already added alongside Task 2 because recorder.ts imports it at module top. Inline SW cleanup (Rule 3 — blocking dependency, plan acceptance gates on `npx tsc --noEmit` exit 0): - Remove src/background/index.ts VIDEO_CHUNK + VIDEO_CHUNK_SAVED case branches (refs to deleted union members). - Remove now-unreferenced loadChunkFromIndexedDB / openIndexedDB (only reachable from the deleted VIDEO_CHUNK_SAVED branch). - Remove now-unreferenced addVideoChunkFromBlob / cleanupVideoBuffer / firstChunkSaved / VIDEO_BUFFER_DURATION_MS constant (the SW-side ring buffer now lives in src/offscreen/recorder.ts per D-16). - Keep SW-side `videoBuffer: VideoChunk[] = []` as a placeholder; Plan 04 wires it to fetch from offscreen over the keepalive port. The remaining `getVideoBuffer` + `saveArchive` callers continue to compile against the empty array until Plan 04 lands. - Plan 05 owns the broader SW shell cleanup. Verification (post-commit): - npx vitest run tests/offscreen/ring-buffer.test.ts tests/offscreen/codec-check.test.ts → 6/6 PASS - npx tsc --noEmit → exit 0 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,12 +9,11 @@ import JSZip from 'jszip';
|
|||||||
|
|
||||||
const logger = new Logger('Main');
|
const logger = new Logger('Main');
|
||||||
|
|
||||||
// Константы
|
|
||||||
const VIDEO_BUFFER_DURATION_MS = 30 * 1000; // 30 секунд
|
|
||||||
|
|
||||||
// Состояние
|
// Состояние
|
||||||
|
// videoBuffer is a placeholder array on the SW side; Plan 04 wires it to
|
||||||
|
// fetch from the offscreen recorder over the 'video-keepalive' port.
|
||||||
|
// Until then it stays empty (the offscreen owns the real buffer per D-16).
|
||||||
let videoBuffer: VideoChunk[] = [];
|
let videoBuffer: VideoChunk[] = [];
|
||||||
let firstChunkSaved = false; // Флаг что первый чанк уже сохранен
|
|
||||||
let isRecording = false;
|
let isRecording = false;
|
||||||
let offscreenCreated = false;
|
let offscreenCreated = false;
|
||||||
let lastScreenshotTime = 0;
|
let lastScreenshotTime = 0;
|
||||||
@@ -22,57 +21,9 @@ let cachedScreenshot: Blob | null = null;
|
|||||||
// userEvents хранится только в content script
|
// userEvents хранится только в content script
|
||||||
// Для архивации получаем его оттуда
|
// Для архивации получаем его оттуда
|
||||||
|
|
||||||
// Кольцевой буфер видео
|
// addVideoChunkFromBlob / cleanupVideoBuffer / VIDEO_BUFFER_DURATION_MS
|
||||||
function addVideoChunkFromBlob(blob: Blob) {
|
// removed in plan 01-03: the ring buffer now lives in src/offscreen/recorder.ts
|
||||||
logger.log(`Processing video chunk from Blob, size: ${blob.size} bytes, type: ${blob.type}`);
|
// (D-16). Plan 05 collapses the remaining SW shell further.
|
||||||
|
|
||||||
const chunk: VideoChunk = {
|
|
||||||
data: blob,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
isFirst: !firstChunkSaved // Первый чанк помечаем как isFirst
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!firstChunkSaved) {
|
|
||||||
firstChunkSaved = true;
|
|
||||||
logger.log(`This is the FIRST video chunk (WebM header), size: ${blob.size} bytes`);
|
|
||||||
}
|
|
||||||
|
|
||||||
videoBuffer.push(chunk);
|
|
||||||
logger.log(`Added video chunk, buffer size: ${videoBuffer.length}, chunk size: ${blob.size} bytes, isFirst: ${chunk.isFirst}`);
|
|
||||||
|
|
||||||
// Удаляем старые чанки
|
|
||||||
cleanupVideoBuffer();
|
|
||||||
}
|
|
||||||
|
|
||||||
function cleanupVideoBuffer() {
|
|
||||||
const now = Date.now();
|
|
||||||
const beforeCount = videoBuffer.length;
|
|
||||||
|
|
||||||
logger.log(`Cleaning up buffer, current size: ${beforeCount}`);
|
|
||||||
|
|
||||||
// Всегда сохраняем первый чанк (WebM заголовок, помечен как isFirst)
|
|
||||||
// Остальные чанки фильтруем по времени (старше 30 секунд удаляем)
|
|
||||||
videoBuffer = videoBuffer.filter(chunk => {
|
|
||||||
// Всегда оставляем первый чанк (заголовок)
|
|
||||||
if (chunk.isFirst) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// Остальные - только если моложе 30 секунд
|
|
||||||
const age = now - chunk.timestamp;
|
|
||||||
const keep = age < VIDEO_BUFFER_DURATION_MS;
|
|
||||||
if (!keep) {
|
|
||||||
logger.log(`Removing chunk, age: ${age}ms, limit: ${VIDEO_BUFFER_DURATION_MS}ms`);
|
|
||||||
}
|
|
||||||
return keep;
|
|
||||||
});
|
|
||||||
|
|
||||||
const removed = beforeCount - videoBuffer.length;
|
|
||||||
if (removed > 0) {
|
|
||||||
logger.log(`Removed ${removed} old video chunks, buffer: ${videoBuffer.length}`);
|
|
||||||
} else {
|
|
||||||
logger.log(`No chunks removed, buffer: ${videoBuffer.length}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Создание offscreen документа
|
// Создание offscreen документа
|
||||||
async function ensureOffscreen() {
|
async function ensureOffscreen() {
|
||||||
@@ -454,23 +405,13 @@ chrome.runtime.onMessage.addListener((message: Message, _sender, sendResponse) =
|
|||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case 'VIDEO_CHUNK':
|
// VIDEO_CHUNK and VIDEO_CHUNK_SAVED handlers removed in plan 01-03:
|
||||||
const videoData = (message as any).data;
|
// - the offscreen recorder now owns the buffer (D-16);
|
||||||
const videoTimestamp = (message as any).timestamp;
|
// - chunks no longer travel via chrome.runtime.sendMessage (D-19);
|
||||||
if (videoData && videoData.size > 0) {
|
// - IndexedDB SW-side plumbing is the audit P0 #2 broken path.
|
||||||
logger.log(`Received video chunk from offscreen, size: ${videoData.size} bytes, timestamp: ${videoTimestamp}`);
|
// loadChunkFromIndexedDB / openIndexedDB also removed inline (they
|
||||||
addVideoChunkFromBlob(videoData);
|
// were only reachable from the deleted VIDEO_CHUNK_SAVED branch).
|
||||||
} else {
|
// Plan 05 collapses the remaining SW dead code further.
|
||||||
logger.warn('Received empty or invalid video chunk');
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
|
|
||||||
case 'VIDEO_CHUNK_SAVED':
|
|
||||||
const chunkId = (message as any).chunkId;
|
|
||||||
const size = (message as any).size;
|
|
||||||
logger.log(`Video chunk ${chunkId} saved, size: ${size} bytes, loading from IndexedDB...`);
|
|
||||||
loadChunkFromIndexedDB(chunkId);
|
|
||||||
return false;
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
logger.warn('Unknown message type:', message.type);
|
logger.warn('Unknown message type:', message.type);
|
||||||
@@ -478,47 +419,6 @@ chrome.runtime.onMessage.addListener((message: Message, _sender, sendResponse) =
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// IndexedDB для загрузки видеочанков
|
|
||||||
async function loadChunkFromIndexedDB(chunkId: number) {
|
|
||||||
try {
|
|
||||||
const db = await openIndexedDB();
|
|
||||||
const transaction = db.transaction(['chunks'], 'readonly');
|
|
||||||
const store = transaction.objectStore('chunks');
|
|
||||||
const request = store.get(chunkId);
|
|
||||||
|
|
||||||
request.onsuccess = () => {
|
|
||||||
const record = request.result;
|
|
||||||
if (record) {
|
|
||||||
logger.log(`Loaded chunk ${chunkId} from IndexedDB, size: ${record.data.size} bytes`);
|
|
||||||
addVideoChunkFromBlob(record.data);
|
|
||||||
} else {
|
|
||||||
logger.error(`Chunk ${chunkId} not found in IndexedDB`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
request.onerror = () => {
|
|
||||||
logger.error(`Error loading chunk ${chunkId} from IndexedDB:`, request.error);
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`Failed to open IndexedDB:`, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function openIndexedDB(): Promise<IDBDatabase> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const request = indexedDB.open('VideoRecorderDB', 1);
|
|
||||||
|
|
||||||
request.onerror = () => reject(request.error);
|
|
||||||
request.onsuccess = () => resolve(request.result);
|
|
||||||
request.onupgradeneeded = (event) => {
|
|
||||||
const db = (event.target as IDBOpenDBRequest).result;
|
|
||||||
if (!db.objectStoreNames.contains('chunks')) {
|
|
||||||
db.createObjectStore('chunks', { keyPath: 'id' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Инициализация
|
// Инициализация
|
||||||
function initialize() {
|
function initialize() {
|
||||||
logger.log('Service Worker initializing');
|
logger.log('Service Worker initializing');
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ export type MessageType =
|
|||||||
| 'START_RECORDING'
|
| 'START_RECORDING'
|
||||||
| 'STOP_RECORDING'
|
| 'STOP_RECORDING'
|
||||||
| 'RECORDING_ERROR'
|
| 'RECORDING_ERROR'
|
||||||
| 'VIDEO_CHUNK'
|
|
||||||
| 'VIDEO_CHUNK_SAVED'
|
|
||||||
| 'OFFSCREEN_READY';
|
| 'OFFSCREEN_READY';
|
||||||
|
|
||||||
export interface Message<T = any> {
|
export interface Message<T = any> {
|
||||||
@@ -23,6 +21,17 @@ export interface Message<T = any> {
|
|||||||
tabId?: number;
|
tabId?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Типы сообщений в long-lived port (offscreen ↔ SW; D-17 / Plan 04)
|
||||||
|
export type PortMessageType =
|
||||||
|
| 'PING'
|
||||||
|
| 'REQUEST_BUFFER'
|
||||||
|
| 'BUFFER';
|
||||||
|
|
||||||
|
export interface PortMessage {
|
||||||
|
type: PortMessageType;
|
||||||
|
chunks?: VideoChunk[];
|
||||||
|
}
|
||||||
|
|
||||||
// Видеобуфер
|
// Видеобуфер
|
||||||
export interface VideoChunk {
|
export interface VideoChunk {
|
||||||
data: Blob;
|
data: Blob;
|
||||||
|
|||||||
Reference in New Issue
Block a user