Milestone v1 (v2.0.0): Mokosh — Session Capture #1
@@ -1,10 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>Offscreen Page</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<script src="./index.ts" type="module"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
let mediaRecorder: MediaRecorder | null = null;
|
|
||||||
let videoChunks: Blob[] = [];
|
|
||||||
|
|
||||||
// Запись видео
|
|
||||||
async function startRecording(streamId: string) {
|
|
||||||
const stream = await navigator.mediaDevices.getUserMedia({
|
|
||||||
video: {
|
|
||||||
mandatory: {
|
|
||||||
chromeMediaSource: 'tab',
|
|
||||||
chromeMediaSourceId: streamId
|
|
||||||
}
|
|
||||||
} as any,
|
|
||||||
audio: false
|
|
||||||
});
|
|
||||||
|
|
||||||
mediaRecorder = new MediaRecorder(stream);
|
|
||||||
|
|
||||||
mediaRecorder.ondataavailable = (event) => {
|
|
||||||
if (event.data && event.data.size > 0) {
|
|
||||||
videoChunks.push(event.data);
|
|
||||||
chrome.runtime.sendMessage({
|
|
||||||
type: 'VIDEO_CHUNK',
|
|
||||||
data: event.data,
|
|
||||||
timestamp: Date.now()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
mediaRecorder.start(1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Остановка записи
|
|
||||||
function stopRecording() {
|
|
||||||
if (mediaRecorder && mediaRecorder.state !== 'inactive') {
|
|
||||||
mediaRecorder.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Получение чанков
|
|
||||||
function getChunks(): Blob[] {
|
|
||||||
return videoChunks;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Обработка сообщений
|
|
||||||
chrome.runtime.onMessage.addListener((message) => {
|
|
||||||
switch (message.type) {
|
|
||||||
case 'START_RECORDING':
|
|
||||||
startRecording(message.streamId);
|
|
||||||
break;
|
|
||||||
case 'STOP_RECORDING':
|
|
||||||
stopRecording();
|
|
||||||
break;
|
|
||||||
case 'GET_CHUNKS':
|
|
||||||
chrome.runtime.sendMessage({
|
|
||||||
type: 'CHUNKS_RESPONSE',
|
|
||||||
chunks: getChunks()
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
222
vite.config.ts
222
vite.config.ts
@@ -1,6 +1,6 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite';
|
||||||
import { crx } from '@crxjs/vite-plugin'
|
import { crx } from '@crxjs/vite-plugin';
|
||||||
import manifest from './manifest.json'
|
import manifest from './manifest.json';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
@@ -10,218 +10,12 @@ export default defineConfig({
|
|||||||
injectCss: false,
|
injectCss: false,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
{
|
|
||||||
name: 'copy-offscreen',
|
|
||||||
generateBundle() {
|
|
||||||
this.emitFile({
|
|
||||||
type: 'asset',
|
|
||||||
fileName: 'offscreen/index.html',
|
|
||||||
source: `
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>Offscreen Page</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<script type="module" src="../assets/offscreen.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
`
|
|
||||||
});
|
|
||||||
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: {
|
build: {
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
output: {
|
input: {
|
||||||
manualChunks: undefined
|
offscreen: 'src/offscreen/index.html',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
copyPublicDir: true,
|
},
|
||||||
publicDir: 'public',
|
});
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|||||||
Reference in New Issue
Block a user