Milestone v1 (v2.0.0): Mokosh — Session Capture #1
@@ -16,6 +16,16 @@ let isRecording = false;
|
||||
let offscreenCreated = false;
|
||||
let lastScreenshotTime = 0;
|
||||
let cachedScreenshot: Blob | null = null;
|
||||
// Port from offscreen (D-17). Re-assigned on every (re)connect.
|
||||
let videoPort: chrome.runtime.Port | null = null;
|
||||
// Offscreen readiness Promise — set up at module load, resolved on first
|
||||
// OFFSCREEN_READY message (Pattern 4). startVideoCapture awaits this before
|
||||
// sending START_RECORDING, so we never lose the popup's transient activation
|
||||
// to a race with the offscreen bootstrap.
|
||||
let offscreenReadyResolve: (() => void) | null = null;
|
||||
let offscreenReady: Promise<void> = new Promise((res) => {
|
||||
offscreenReadyResolve = res;
|
||||
});
|
||||
// userEvents хранится только в content script
|
||||
// Для архивации получаем его оттуда
|
||||
|
||||
@@ -54,6 +64,61 @@ async function ensureOffscreen() {
|
||||
}
|
||||
}
|
||||
|
||||
// SW-side port host (D-17, RESEARCH.md Pattern 5). The offscreen opens this
|
||||
// port on bootstrap and reconnects on disconnect. We use it for: (a)
|
||||
// keepalive traffic (PING) — Chrome 110+ resets the SW idle timer on every
|
||||
// port message; (b) on-demand REQUEST_BUFFER round-trip during SAVE_ARCHIVE.
|
||||
chrome.runtime.onConnect.addListener((port) => {
|
||||
// T-1-04 mitigation: only accept ports from this extension
|
||||
if (port.name !== 'video-keepalive') {
|
||||
return;
|
||||
}
|
||||
if (port.sender?.id !== chrome.runtime.id) {
|
||||
logger.warn('Rejecting port with mismatched sender:', port.sender?.id);
|
||||
port.disconnect();
|
||||
return;
|
||||
}
|
||||
logger.log('Offscreen port connected');
|
||||
videoPort = port;
|
||||
port.onDisconnect.addListener(() => {
|
||||
logger.log('Offscreen port disconnected; offscreen will reconnect');
|
||||
videoPort = null;
|
||||
});
|
||||
// Inbound traffic is mostly PING (ignored) and BUFFER (handled by the
|
||||
// per-request listener installed in getVideoBufferFromOffscreen).
|
||||
});
|
||||
|
||||
const BUFFER_FETCH_TIMEOUT_MS = 2_000;
|
||||
|
||||
async function getVideoBufferFromOffscreen(): Promise<VideoBufferResponse> {
|
||||
if (videoPort === null) {
|
||||
logger.warn('No offscreen port available; returning empty buffer');
|
||||
return { chunks: [] };
|
||||
}
|
||||
const port = videoPort;
|
||||
return new Promise<VideoBufferResponse>((resolve) => {
|
||||
const timer = setTimeout(() => {
|
||||
port.onMessage.removeListener(handler);
|
||||
logger.warn(`Buffer fetch timed out after ${BUFFER_FETCH_TIMEOUT_MS} ms`);
|
||||
resolve({ chunks: [] });
|
||||
}, BUFFER_FETCH_TIMEOUT_MS);
|
||||
const handler = (msg: unknown) => {
|
||||
if (
|
||||
typeof msg === 'object' &&
|
||||
msg !== null &&
|
||||
(msg as { type?: unknown }).type === 'BUFFER'
|
||||
) {
|
||||
clearTimeout(timer);
|
||||
port.onMessage.removeListener(handler);
|
||||
const chunks = (msg as { chunks?: VideoChunk[] }).chunks ?? [];
|
||||
resolve({ chunks });
|
||||
}
|
||||
};
|
||||
port.onMessage.addListener(handler);
|
||||
port.postMessage({ type: 'REQUEST_BUFFER' });
|
||||
});
|
||||
}
|
||||
|
||||
// Начало записи видео
|
||||
async function startVideoCapture() {
|
||||
if (isRecording) {
|
||||
@@ -73,6 +138,11 @@ async function startVideoCapture() {
|
||||
|
||||
// Создаём offscreen документ (с reason из D-02)
|
||||
await ensureOffscreen();
|
||||
// Ждём, пока offscreen зарегистрирует свой onMessage listener
|
||||
// (RESEARCH.md Pattern 4). Иначе гонка: START_RECORDING летит раньше,
|
||||
// чем offscreen готов его принять, и Chrome бросает "Receiving end
|
||||
// does not exist".
|
||||
await offscreenReady;
|
||||
|
||||
// Просим offscreen запустить запись — getDisplayMedia вызывается там
|
||||
// (D-01: больше нет SW-side stream-id юзаства).
|
||||
@@ -101,11 +171,8 @@ async function startVideoCapture() {
|
||||
// Старая alarms-based реализация удалена: alarm callbacks не сбрасывали SW idle
|
||||
// timer (audit P1 #8), а порт сбрасывает таймер на каждое сообщение.
|
||||
|
||||
// Получение видеобуфера (временный синхронный стаб; Task 2 заменит его
|
||||
// на асинхронный запрос к offscreen через long-lived port).
|
||||
function getVideoBuffer(): VideoBufferResponse {
|
||||
return { chunks: [] };
|
||||
}
|
||||
// Получение видеобуфера — port-based (getVideoBufferFromOffscreen объявлен
|
||||
// выше). Старый синхронный SW-локальный буфер удалён в Task 1 этого плана.
|
||||
|
||||
// Получение скриншота активной вкладки
|
||||
async function captureScreenshot(): Promise<Blob> {
|
||||
@@ -264,9 +331,9 @@ async function saveArchive() {
|
||||
logger.log('Capturing screenshot...');
|
||||
const screenshot = await captureScreenshot();
|
||||
|
||||
// Получаем видео буфер
|
||||
const videoBuffer = getVideoBuffer();
|
||||
logger.log(`Video buffer: ${videoBuffer.chunks.length} chunks`);
|
||||
// Получаем видео буфер из offscreen через long-lived port (D-17)
|
||||
const videoBufferResp = await getVideoBufferFromOffscreen();
|
||||
logger.log(`Video buffer: ${videoBufferResp.chunks.length} chunks`);
|
||||
|
||||
// Получаем rrweb события от content script
|
||||
logger.log(`Requesting rrweb events from content script on tab ${tab.id} (${tab.url})...`);
|
||||
@@ -303,7 +370,7 @@ async function saveArchive() {
|
||||
|
||||
// Создаем архив
|
||||
const archiveBlob = await createArchive(
|
||||
videoBuffer,
|
||||
videoBufferResp,
|
||||
rrwebEvents,
|
||||
userEvents,
|
||||
screenshot
|
||||
@@ -327,7 +394,14 @@ async function saveArchive() {
|
||||
// REQUEST_PERMISSIONS теперь просто запускает запись и возвращает granted=true.
|
||||
|
||||
// Обработка сообщений
|
||||
chrome.runtime.onMessage.addListener((message: Message, _sender, sendResponse) => {
|
||||
chrome.runtime.onMessage.addListener((message: Message, sender, sendResponse) => {
|
||||
// T-1-NEW-05-01 mitigation: only accept onMessage traffic from this
|
||||
// extension (popup, content script, offscreen). External callers (other
|
||||
// extensions, web pages) are silently dropped.
|
||||
if (sender.id !== chrome.runtime.id) {
|
||||
logger.warn('Rejecting message with mismatched sender:', sender.id);
|
||||
return false;
|
||||
}
|
||||
logger.log('Received message:', message.type, message);
|
||||
|
||||
switch (message.type) {
|
||||
@@ -349,8 +423,8 @@ chrome.runtime.onMessage.addListener((message: Message, _sender, sendResponse) =
|
||||
return true;
|
||||
|
||||
case 'GET_VIDEO_BUFFER':
|
||||
sendResponse(getVideoBuffer());
|
||||
return false;
|
||||
getVideoBufferFromOffscreen().then((resp) => sendResponse(resp));
|
||||
return true;
|
||||
|
||||
case 'SAVE_ARCHIVE':
|
||||
saveArchive().then(result => {
|
||||
@@ -358,6 +432,12 @@ chrome.runtime.onMessage.addListener((message: Message, _sender, sendResponse) =
|
||||
});
|
||||
return true;
|
||||
|
||||
case 'OFFSCREEN_READY':
|
||||
logger.log('OFFSCREEN_READY received');
|
||||
offscreenReadyResolve?.();
|
||||
offscreenReadyResolve = null;
|
||||
return false;
|
||||
|
||||
// Legacy chunk-streaming and IndexedDB save/load handlers were removed
|
||||
// in Plan 01-03:
|
||||
// - the offscreen recorder now owns the buffer (D-16);
|
||||
@@ -373,14 +453,38 @@ chrome.runtime.onMessage.addListener((message: Message, _sender, sendResponse) =
|
||||
});
|
||||
|
||||
// Инициализация
|
||||
function initialize() {
|
||||
async function initialize() {
|
||||
logger.log('Service Worker initializing');
|
||||
// Audit P1 #8: after the SW respawns (e.g. after Chrome wakes it from idle),
|
||||
// the offscreen document may still exist while our offscreenCreated flag
|
||||
// resets to false. Ask Chrome the ground truth so we don't end up trying
|
||||
// to createDocument over an existing one. Cheap and idempotent.
|
||||
try {
|
||||
if (typeof chrome.offscreen?.hasDocument === 'function') {
|
||||
const exists = await chrome.offscreen.hasDocument();
|
||||
if (exists) {
|
||||
offscreenCreated = true;
|
||||
logger.log('Existing offscreen document detected on SW init');
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.warn('chrome.offscreen.hasDocument check failed:', err);
|
||||
}
|
||||
logger.log('Service Worker initialized');
|
||||
}
|
||||
|
||||
// Запуск при установке
|
||||
chrome.runtime.onInstalled.addListener((details) => {
|
||||
logger.log('Extension installed/updated:', details.reason);
|
||||
// RESEARCH.md Runtime State Inventory — clean up orphaned IndexedDB from
|
||||
// pre-Phase-01 builds. Idempotent: no-op if DB never existed.
|
||||
// T-1-NEW-05-02 mitigation.
|
||||
try {
|
||||
indexedDB.deleteDatabase('VideoRecorderDB');
|
||||
logger.log('Cleaned up orphaned VideoRecorderDB (if present)');
|
||||
} catch (e) {
|
||||
logger.warn('IDB cleanup failed:', e);
|
||||
}
|
||||
initialize();
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user