import { defineConfig } from 'vite' import { crx } from '@crxjs/vite-plugin' import manifest from './manifest.json' export default defineConfig({ plugins: [ crx({ manifest, contentScripts: { injectCss: false, }, }), { name: 'copy-offscreen', generateBundle() { this.emitFile({ type: 'asset', fileName: 'offscreen/index.html', source: ` Offscreen Page ` }); this.emitFile({ type: 'asset', fileName: 'assets/offscreen.js', source: ` let mediaRecorder = null; let videoChunks = []; let chunkCount = 0; // IndexedDB для хранения видеочанков let db = null; function openIndexedDB() { return new Promise((resolve, reject) => { const request = indexedDB.open('VideoRecorderDB', 1); request.onerror = () => reject(request.error); request.onsuccess = () => { db = request.result; resolve(db); }; request.onupgradeneeded = (event) => { const db = event.target.result; if (!db.objectStoreNames.contains('chunks')) { db.createObjectStore('chunks', { keyPath: 'id' }); } }; }); } async function saveChunkToIndexedDB(blob, chunkId) { if (!db) { await openIndexedDB(); } return new Promise((resolve, reject) => { const transaction = db.transaction(['chunks'], 'readwrite'); const store = transaction.objectStore('chunks'); const request = store.put({ id: chunkId, data: blob, timestamp: Date.now() }); request.onerror = () => reject(request.error); request.onsuccess = () => { console.log('[Offscreen] Chunk', chunkId, 'saved to IndexedDB, size:', blob.size); // Отправляем уведомление в Service Worker chrome.runtime.sendMessage({ type: 'VIDEO_CHUNK_SAVED', chunkId: chunkId, size: blob.size }); resolve(); }; }); } async function clearOldChunks() { if (!db) { await openIndexedDB(); } return new Promise((resolve, reject) => { const transaction = db.transaction(['chunks'], 'readwrite'); const store = transaction.objectStore('chunks'); const request = store.clear(); request.onerror = () => reject(request.error); request.onsuccess = () => { console.log('[Offscreen] Cleared old chunks from IndexedDB'); resolve(); }; }); } chrome.runtime.onMessage.addListener((message) => { console.log('[Offscreen] Received message:', message.type); switch (message.type) { case 'START_RECORDING': startRecording(message.streamId); break; case 'STOP_RECORDING': stopRecording(); break; } }); async function startRecording(streamId) { console.log('[Offscreen] Starting recording with streamId:', streamId); // Инициализируем IndexedDB await openIndexedDB(); console.log('[Offscreen] IndexedDB initialized'); // Очищаем старые чанки await clearOldChunks(); try { const stream = await navigator.mediaDevices.getUserMedia({ video: { mandatory: { chromeMediaSource: 'tab', chromeMediaSourceId: streamId } }, audio: false }); console.log('[Offscreen] Stream created, tracks:', stream.getTracks().length); const videoTrack = stream.getVideoTracks()[0]; if (videoTrack) { console.log('[Offscreen] Video track settings:', videoTrack.getSettings()); console.log('[Offscreen] Video track readyState:', videoTrack.readyState); console.log('[Offscreen] Video track enabled:', videoTrack.enabled); } // Пробуем разные кодеки const codecs = [ 'video/webm; codecs=vp9', 'video/webm; codecs=vp8', 'video/webm; codecs=h264', 'video/webm' ]; let mediaRecorder = null; for (const codec of codecs) { try { mediaRecorder = new MediaRecorder(stream, { mimeType: codec }); console.log('[Offscreen] MediaRecorder created with codec:', codec, 'state:', mediaRecorder.state); break; } catch (e) { console.log('[Offscreen] Codec', codec, 'not supported:', e.message); } } if (!mediaRecorder) { mediaRecorder = new MediaRecorder(stream); console.log('[Offscreen] Using default MediaRecorder'); } mediaRecorder.ondataavailable = (event) => { console.log('[Offscreen] Data available:', event.data.size, 'bytes, type:', event.data.type); if (event.data && event.data.size > 0) { chunkCount++; console.log('[Offscreen] Sending chunk', chunkCount, 'to background, size:', event.data.size); // Сохраняем в indexedDB вместо передачи через сообщения saveChunkToIndexedDB(event.data, chunkCount); } else { console.log('[Offscreen] Data available but size is 0, skipping'); } }; mediaRecorder.onerror = (event) => { console.error('[Offscreen] MediaRecorder error:', event); }; mediaRecorder.onstart = () => { console.log('[Offscreen] MediaRecorder started, state:', mediaRecorder.state); }; mediaRecorder.onstop = () => { console.log('[Offscreen] MediaRecorder stopped, state:', mediaRecorder.state, 'total chunks:', chunkCount); }; // Используем интервал 200мс для более частого получения чанков с ключевыми кадрами mediaRecorder.start(200); console.log('[Offscreen] Recording started with interval 200ms'); } catch (error) { console.error('[Offscreen] Error starting recording:', error); } } function stopRecording() { if (mediaRecorder && mediaRecorder.state !== 'inactive') { mediaRecorder.stop(); console.log('[Offscreen] Recording stopped'); } } ` }); } } ], build: { rollupOptions: { output: { manualChunks: undefined } }, copyPublicDir: true, publicDir: 'public', } })