Milestone v1 (v2.0.0): Mokosh — Session Capture #1

Merged
strategy155 merged 297 commits from gsd/phase-04-harden-clean-up-optional into main 2026-05-31 15:34:17 +00:00
2 changed files with 24 additions and 115 deletions
Showing only changes of commit c5828d38ef - Show all commits

View File

@@ -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');

View File

@@ -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;